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, useRef, useCallback, useMemo, Fragment } from 'react';
1
+ import { useState, useEffect, useRef, useCallback, useMemo, Fragment, Children } from 'react';
2
2
  import ReactMarkdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import remarkBreaks from 'remark-breaks';
@@ -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,47 @@ 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
+ dataBase64?: string;
63
+ };
64
+
65
+ type ImportedAttachment = {
66
+ relativePath: string;
67
+ fileName: string;
68
+ size: number;
69
+ mimeType?: string;
70
+ };
71
+
72
+ const formatFileSize = (size: number): string => {
73
+ if (size < 1024) return `${size} B`;
74
+ const kb = size / 1024;
75
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
76
+ const mb = kb / 1024;
77
+ return `${mb.toFixed(1)} MB`;
78
+ };
79
+
80
+ const buildAttachmentSummary = (attachments: ImportedAttachment[]): string => {
81
+ if (attachments.length === 0) return '';
82
+ const lines = attachments.map((attachment) => (
83
+ `- ${attachment.fileName} (${attachment.relativePath})`
84
+ ));
85
+ return `Attached files (relative to workspace):\n${lines.join('\n')}`;
86
+ };
87
+
88
+ const composeMessageWithAttachments = (text: string, attachments: ImportedAttachment[]): string => {
89
+ const base = text.trim() || 'Please review the attached files.';
90
+ const summary = buildAttachmentSummary(attachments);
91
+ return summary ? `${base}\n\n${summary}` : base;
92
+ };
93
+
52
94
  type MentionOption = {
53
95
  type: 'agent' | 'everyone';
54
96
  id: string;
@@ -63,6 +105,8 @@ const normalizeMentionSearch = (value: string): string =>
63
105
  import { ApprovalDialog } from './ApprovalDialog';
64
106
  import { SkillParameterModal } from './SkillParameterModal';
65
107
  import { FileViewer } from './FileViewer';
108
+ import { ThemeIcon } from './ThemeIcon';
109
+ import { AlertTriangleIcon, BookIcon, ChartIcon, CheckIcon, ClipboardIcon, EditIcon, FolderIcon, InfoIcon, SearchIcon, UsersIcon, XIcon } from './LineIcons';
66
110
  import { CommandOutput } from './CommandOutput';
67
111
  import { CanvasPreview } from './CanvasPreview';
68
112
 
@@ -301,9 +345,235 @@ function MessageSpeakButton({ text, voiceEnabled }: { text: string; voiceEnabled
301
345
  );
302
346
  }
303
347
 
304
- // Custom components for ReactMarkdown
305
- const markdownComponents = {
306
- code: CodeBlock,
348
+ const HEADING_EMOJI_REGEX = /^([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}][\uFE0F\uFE0E]?)(\s+)?/u;
349
+
350
+ const getHeadingIcon = (emoji: string): React.ReactNode | null => {
351
+ switch (emoji) {
352
+ case '✅':
353
+ return <CheckIcon size={16} />;
354
+ case '❌':
355
+ return <XIcon size={16} />;
356
+ case '⚠️':
357
+ case '⚠':
358
+ return <AlertTriangleIcon size={16} />;
359
+ case 'ℹ️':
360
+ case 'ℹ':
361
+ return <InfoIcon size={16} />;
362
+ default:
363
+ return null;
364
+ }
365
+ };
366
+
367
+ const renderHeading = (Tag: 'h1' | 'h2' | 'h3') => {
368
+ return ({ children, ...props }: any) => {
369
+ const nodes = Children.toArray(children);
370
+ let emoji: string | null = null;
371
+ if (typeof nodes[0] === 'string') {
372
+ const match = (nodes[0] as string).match(HEADING_EMOJI_REGEX);
373
+ if (match) {
374
+ emoji = match[1];
375
+ const nextIcon = getHeadingIcon(emoji);
376
+ if (nextIcon) {
377
+ nodes[0] = (nodes[0] as string).slice(match[0].length);
378
+ return (
379
+ <Tag {...props}>
380
+ <span className="markdown-heading-icon"><ThemeIcon emoji={emoji} icon={nextIcon} /></span>
381
+ {nodes}
382
+ </Tag>
383
+ );
384
+ }
385
+ }
386
+ }
387
+ const icon = emoji ? getHeadingIcon(emoji) : null;
388
+ return (
389
+ <Tag {...props}>
390
+ {icon && <span className="markdown-heading-icon"><ThemeIcon emoji={emoji} icon={icon} /></span>}
391
+ {nodes}
392
+ </Tag>
393
+ );
394
+ };
395
+ };
396
+
397
+ const isExternalHttpLink = (href: string): boolean =>
398
+ href.startsWith('http://') || href.startsWith('https://');
399
+
400
+ const FILE_EXTENSIONS = new Set([
401
+ 'txt', 'md', 'markdown', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'tsv', 'ppt', 'pptx',
402
+ 'json', 'yaml', 'yml', 'xml', 'html', 'htm',
403
+ 'js', 'ts', 'tsx', 'jsx', 'css', 'scss', 'less', 'sass',
404
+ 'py', 'rb', 'go', 'rs', 'java', 'kt', 'swift', 'cpp', 'c', 'h', 'hpp',
405
+ 'sh', 'bash', 'zsh', 'ps1', 'toml', 'ini', 'env', 'lock', 'log',
406
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'tiff',
407
+ 'mp3', 'wav', 'm4a', 'mp4', 'mov', 'avi', 'mkv',
408
+ 'zip', 'tar', 'gz', 'tgz', 'rar', '7z',
409
+ ]);
410
+
411
+ const getTextContent = (node: React.ReactNode): string => {
412
+ if (typeof node === 'string') return node;
413
+ if (Array.isArray(node)) return node.map(getTextContent).join('');
414
+ if (node && typeof node === 'object' && 'props' in node) {
415
+ return getTextContent((node as { props: { children?: React.ReactNode } }).props.children);
416
+ }
417
+ return '';
418
+ };
419
+
420
+ const stripHttpScheme = (value: string): string =>
421
+ value.replace(/^https?:\/\//, '');
422
+
423
+ const looksLikeLocalFilePath = (value: string): boolean => {
424
+ const trimmed = value.trim();
425
+ if (!trimmed) return false;
426
+ if (trimmed.startsWith('#')) return false;
427
+ if (trimmed.startsWith('file://')) return true;
428
+ if (trimmed.startsWith('mailto:') || trimmed.startsWith('tel:')) return false;
429
+ if (trimmed.includes('://') || trimmed.startsWith('www.')) return false;
430
+ if (trimmed.includes('@')) return false;
431
+ if (trimmed.startsWith('./') || trimmed.startsWith('../') || trimmed.startsWith('~/') || trimmed.startsWith('/')) return true;
432
+ if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
433
+ if (trimmed.includes('/') || trimmed.includes('\\')) return true;
434
+ const extMatch = trimmed.match(/\.([a-zA-Z0-9]{1,8})$/);
435
+ if (!extMatch) return false;
436
+ return FILE_EXTENSIONS.has(extMatch[1].toLowerCase());
437
+ };
438
+
439
+ const isFileLink = (href: string): boolean => {
440
+ if (!href) return false;
441
+ if (href.startsWith('#')) return false;
442
+ if (isExternalHttpLink(href)) return false;
443
+ if (href.startsWith('mailto:') || href.startsWith('tel:')) return false;
444
+ if (href.startsWith('file://')) return true;
445
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href)) return false;
446
+ return true;
447
+ };
448
+
449
+ const normalizeFileHref = (href: string): string => {
450
+ if (!href) return href;
451
+ if (href.startsWith('file://')) {
452
+ const rawPath = href.replace(/^file:\/\//, '');
453
+ const decoded = (() => {
454
+ try {
455
+ return decodeURIComponent(rawPath);
456
+ } catch {
457
+ return rawPath;
458
+ }
459
+ })();
460
+ return decoded.replace(/^\/([a-zA-Z]:\/)/, '$1').split(/[?#]/)[0];
461
+ }
462
+ return href.split(/[?#]/)[0];
463
+ };
464
+
465
+ const resolveFileLinkTarget = (href: string, linkText: string): string | null => {
466
+ const trimmedText = linkText.trim();
467
+ const trimmedHref = href.trim();
468
+
469
+ if (looksLikeLocalFilePath(trimmedText)) {
470
+ const strippedHref = stripHttpScheme(trimmedHref).replace(/\/$/, '');
471
+ if (trimmedHref === trimmedText || strippedHref === trimmedText) {
472
+ return normalizeFileHref(trimmedText);
473
+ }
474
+ }
475
+
476
+ if (looksLikeLocalFilePath(trimmedHref)) {
477
+ return normalizeFileHref(trimmedHref);
478
+ }
479
+
480
+ return null;
481
+ };
482
+
483
+ const buildMarkdownComponents = (options: {
484
+ workspacePath?: string;
485
+ onOpenViewer?: (path: string) => void;
486
+ }) => {
487
+ const { workspacePath, onOpenViewer } = options;
488
+
489
+ const MarkdownLink = ({ href, children, ...props }: any) => {
490
+ if (!href) {
491
+ return <a {...props}>{children}</a>;
492
+ }
493
+
494
+ const linkText = getTextContent(children);
495
+ const fileTarget = resolveFileLinkTarget(href, linkText);
496
+
497
+ if (fileTarget || isFileLink(href)) {
498
+ const filePath = fileTarget ?? normalizeFileHref(href);
499
+ const handleClick = async (e: React.MouseEvent) => {
500
+ e.preventDefault();
501
+ e.stopPropagation();
502
+
503
+ if (onOpenViewer && workspacePath) {
504
+ onOpenViewer(filePath);
505
+ return;
506
+ }
507
+
508
+ if (!workspacePath) return;
509
+
510
+ try {
511
+ const error = await window.electronAPI.openFile(filePath, workspacePath);
512
+ if (error) {
513
+ console.error('Failed to open file:', error);
514
+ }
515
+ } catch (err) {
516
+ console.error('Error opening file:', err);
517
+ }
518
+ };
519
+
520
+ const handleContextMenu = async (e: React.MouseEvent) => {
521
+ e.preventDefault();
522
+ e.stopPropagation();
523
+ if (!workspacePath) return;
524
+ try {
525
+ await window.electronAPI.showInFinder(filePath, workspacePath);
526
+ } catch (err) {
527
+ console.error('Error showing in Finder:', err);
528
+ }
529
+ };
530
+
531
+ return (
532
+ <a
533
+ {...props}
534
+ href={href}
535
+ className={`clickable-file-path ${props.className || ''}`.trim()}
536
+ onClick={handleClick}
537
+ onContextMenu={handleContextMenu}
538
+ title={`${filePath}\n\nClick to preview • Right-click to show in Finder`}
539
+ >
540
+ {children}
541
+ </a>
542
+ );
543
+ }
544
+
545
+ if (isExternalHttpLink(href)) {
546
+ const handleClick = async (e: React.MouseEvent) => {
547
+ e.preventDefault();
548
+ e.stopPropagation();
549
+ try {
550
+ await window.electronAPI.openExternal(href);
551
+ } catch (err) {
552
+ console.error('Error opening link:', err);
553
+ }
554
+ };
555
+ return (
556
+ <a {...props} href={href} onClick={handleClick}>
557
+ {children}
558
+ </a>
559
+ );
560
+ }
561
+
562
+ return (
563
+ <a {...props} href={href}>
564
+ {children}
565
+ </a>
566
+ );
567
+ };
568
+
569
+ // Custom components for ReactMarkdown
570
+ return {
571
+ code: CodeBlock,
572
+ h1: renderHeading('h1'),
573
+ h2: renderHeading('h2'),
574
+ h3: renderHeading('h3'),
575
+ a: MarkdownLink,
576
+ };
307
577
  };
308
578
 
309
579
  const userMarkdownPlugins = [remarkGfm, remarkBreaks];
@@ -313,9 +583,10 @@ interface ModelDropdownProps {
313
583
  models: LLMModelInfo[];
314
584
  selectedModel: string;
315
585
  onModelChange: (model: string) => void;
586
+ onOpenSettings?: (tab?: SettingsTab) => void;
316
587
  }
317
588
 
318
- function ModelDropdown({ models, selectedModel, onModelChange }: ModelDropdownProps) {
589
+ function ModelDropdown({ models, selectedModel, onModelChange, onOpenSettings }: ModelDropdownProps) {
319
590
  const [isOpen, setIsOpen] = useState(false);
320
591
  const [search, setSearch] = useState('');
321
592
  const [highlightedIndex, setHighlightedIndex] = useState(0);
@@ -398,6 +669,12 @@ function ModelDropdown({ models, selectedModel, onModelChange }: ModelDropdownPr
398
669
  setSearch('');
399
670
  };
400
671
 
672
+ const handleOpenProviders = () => {
673
+ setIsOpen(false);
674
+ setSearch('');
675
+ onOpenSettings?.('llm');
676
+ };
677
+
401
678
  return (
402
679
  <div className="model-dropdown-container" ref={containerRef}>
403
680
  <button
@@ -457,6 +734,11 @@ function ModelDropdown({ models, selectedModel, onModelChange }: ModelDropdownPr
457
734
  ))
458
735
  )}
459
736
  </div>
737
+ <div className="model-dropdown-footer">
738
+ <button type="button" className="model-dropdown-provider-btn" onClick={handleOpenProviders}>
739
+ Change provider
740
+ </button>
741
+ </div>
460
742
  </div>
461
743
  )}
462
744
  </div>
@@ -526,7 +808,7 @@ interface GoalModeOptions {
526
808
  maxAttempts?: number;
527
809
  }
528
810
 
529
- type SettingsTab = 'appearance' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'morechannels' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'voice';
811
+ type SettingsTab = 'appearance' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'x' | 'morechannels' | 'integrations' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'voice';
530
812
 
531
813
  interface MainContentProps {
532
814
  task: Task | undefined;
@@ -539,6 +821,7 @@ interface MainContentProps {
539
821
  onSelectWorkspace?: (workspace: Workspace) => void;
540
822
  onOpenSettings?: (tab?: SettingsTab) => void;
541
823
  onStopTask?: () => void;
824
+ onOpenBrowserView?: (url?: string) => void;
542
825
  selectedModel: string;
543
826
  availableModels: LLMModelInfo[];
544
827
  onModelChange: (model: string) => void;
@@ -553,11 +836,15 @@ interface ActiveCommand {
553
836
  startTimestamp: number; // When the command started, for positioning in timeline
554
837
  }
555
838
 
556
- export function MainContent({ task, selectedTaskId, workspace, events, onSendMessage, onCreateTask, onChangeWorkspace, onSelectWorkspace, onOpenSettings, onStopTask, selectedModel, availableModels, onModelChange }: MainContentProps) {
839
+ export function MainContent({ task, selectedTaskId, workspace, events, onSendMessage, onCreateTask, onChangeWorkspace, onSelectWorkspace, onOpenSettings, onStopTask, onOpenBrowserView, selectedModel, availableModels, onModelChange }: MainContentProps) {
557
840
  // Agent personality context for personalized messages
558
841
  const agentContext = useAgentContext();
559
842
  const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
560
843
  const [inputValue, setInputValue] = useState('');
844
+ const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
845
+ const [attachmentError, setAttachmentError] = useState<string | null>(null);
846
+ const [isDraggingFiles, setIsDraggingFiles] = useState(false);
847
+ const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
561
848
  const [agentRoles, setAgentRoles] = useState<AgentRoleData[]>([]);
562
849
  const [mentionQuery, setMentionQuery] = useState('');
563
850
  const [mentionTarget, setMentionTarget] = useState<{ start: number; end: number } | null>(null);
@@ -606,6 +893,10 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
606
893
  },
607
894
  });
608
895
  const [viewerFilePath, setViewerFilePath] = useState<string | null>(null);
896
+ const markdownComponents = useMemo(
897
+ () => buildMarkdownComponents({ workspacePath: workspace?.path, onOpenViewer: setViewerFilePath }),
898
+ [workspace?.path, setViewerFilePath]
899
+ );
609
900
  // Canvas sessions state - track active canvas sessions for current task
610
901
  const [canvasSessions, setCanvasSessions] = useState<CanvasSession[]>([]);
611
902
  // Workspace dropdown state
@@ -940,10 +1231,12 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
940
1231
  if (!showWorkspaceDropdown) {
941
1232
  try {
942
1233
  const workspaces = await window.electronAPI.listWorkspaces();
943
- // Filter out temp workspace and sort by most recently created
1234
+ // Filter out temp workspace and sort by most recently used
944
1235
  const filteredWorkspaces = workspaces
945
1236
  .filter((w: Workspace) => w.id !== TEMP_WORKSPACE_ID)
946
- .sort((a: Workspace, b: Workspace) => b.createdAt - a.createdAt);
1237
+ .sort((a: Workspace, b: Workspace) =>
1238
+ (b.lastUsedAt ?? b.createdAt) - (a.lastUsedAt ?? a.createdAt)
1239
+ );
947
1240
  setWorkspacesList(filteredWorkspaces);
948
1241
  } catch (error) {
949
1242
  console.error('Failed to load workspaces:', error);
@@ -1220,37 +1513,281 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1220
1513
  }
1221
1514
  };
1222
1515
 
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());
1516
+ const reportAttachmentError = (message: string) => {
1517
+ setAttachmentError(message);
1518
+ window.setTimeout(() => setAttachmentError(null), 5000);
1519
+ };
1520
+
1521
+ const readFileAsBase64 = (file: File): Promise<string> =>
1522
+ new Promise((resolve, reject) => {
1523
+ const reader = new FileReader();
1524
+ reader.onload = () => {
1525
+ const result = typeof reader.result === 'string' ? reader.result : '';
1526
+ const [, base64] = result.split(',');
1527
+ if (!base64) {
1528
+ reject(new Error('Failed to read file data.'));
1529
+ return;
1530
+ }
1531
+ resolve(base64);
1532
+ };
1533
+ reader.onerror = () => reject(reader.error || new Error('Failed to read file data.'));
1534
+ reader.readAsDataURL(file);
1535
+ });
1536
+
1537
+ const appendPendingAttachments = (files: PendingAttachment[]) => {
1538
+ if (files.length === 0) return;
1539
+ setPendingAttachments((prev) => {
1540
+ const existingKeys = new Set(
1541
+ prev.map((attachment) => attachment.path || `${attachment.name}-${attachment.size}`)
1542
+ );
1543
+ const next = [...prev];
1544
+ for (const file of files) {
1545
+ const key = file.path || `${file.name}-${file.size}`;
1546
+ if (existingKeys.has(key)) continue;
1547
+ if (next.length >= MAX_ATTACHMENTS) {
1548
+ reportAttachmentError(`You can attach up to ${MAX_ATTACHMENTS} files.`);
1549
+ break;
1550
+ }
1551
+ next.push({
1552
+ ...file,
1553
+ id: file.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1554
+ });
1555
+ existingKeys.add(key);
1246
1556
  }
1247
- setInputValue('');
1248
- setMentionOpen(false);
1249
- setMentionQuery('');
1250
- setMentionTarget(null);
1557
+ return next;
1558
+ });
1559
+ };
1560
+
1561
+ const handleAttachFiles = async () => {
1562
+ try {
1563
+ const files = await window.electronAPI.selectFiles();
1564
+ if (!files || files.length === 0) return;
1565
+ appendPendingAttachments(
1566
+ files.map((file) => ({
1567
+ ...file,
1568
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1569
+ }))
1570
+ );
1571
+ } catch (error) {
1572
+ console.error('Failed to select files:', error);
1573
+ reportAttachmentError('Failed to add attachments. Please try again.');
1251
1574
  }
1252
1575
  };
1253
1576
 
1577
+ const handleRemoveAttachment = (id: string) => {
1578
+ setPendingAttachments((prev) => prev.filter((attachment) => attachment.id !== id));
1579
+ };
1580
+
1581
+ const isFileDrag = (event: React.DragEvent) =>
1582
+ Array.from(event.dataTransfer.types || []).includes('Files');
1583
+
1584
+ const handleDragOver = (event: React.DragEvent) => {
1585
+ if (!isFileDrag(event)) return;
1586
+ event.preventDefault();
1587
+ setIsDraggingFiles(true);
1588
+ };
1589
+
1590
+ const handleDragLeave = (event: React.DragEvent) => {
1591
+ if (!isFileDrag(event)) return;
1592
+ event.preventDefault();
1593
+ setIsDraggingFiles(false);
1594
+ };
1595
+
1596
+ const handleDrop = async (event: React.DragEvent) => {
1597
+ if (!isFileDrag(event)) return;
1598
+ event.preventDefault();
1599
+ setIsDraggingFiles(false);
1600
+
1601
+ const droppedFiles = Array.from(event.dataTransfer.files || []);
1602
+ try {
1603
+ const pending = await Promise.all(
1604
+ droppedFiles.map(async (file) => {
1605
+ const filePath = (file as File & { path?: string }).path;
1606
+ if (filePath) {
1607
+ return {
1608
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1609
+ path: filePath,
1610
+ name: file.name,
1611
+ size: file.size,
1612
+ mimeType: file.type || undefined,
1613
+ } satisfies PendingAttachment;
1614
+ }
1615
+ const dataBase64 = await readFileAsBase64(file);
1616
+ return {
1617
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1618
+ name: file.name || `drop-${Date.now()}`,
1619
+ size: file.size,
1620
+ mimeType: file.type || undefined,
1621
+ dataBase64,
1622
+ } satisfies PendingAttachment;
1623
+ })
1624
+ );
1625
+
1626
+ appendPendingAttachments(pending);
1627
+ } catch (error) {
1628
+ console.error('Failed to handle dropped files:', error);
1629
+ reportAttachmentError('Failed to attach dropped files.');
1630
+ }
1631
+ };
1632
+
1633
+ const handlePaste = async (event: React.ClipboardEvent) => {
1634
+ const clipboardData = event.clipboardData;
1635
+ let clipboardFiles = Array.from(clipboardData?.files || []);
1636
+ if (clipboardFiles.length === 0 && clipboardData?.items) {
1637
+ Array.from(clipboardData.items).forEach((item: DataTransferItem) => {
1638
+ if (item.kind === 'file') {
1639
+ const file = item.getAsFile();
1640
+ if (file) clipboardFiles.push(file);
1641
+ }
1642
+ });
1643
+ }
1644
+ if (clipboardFiles.length === 0) return;
1645
+ event.preventDefault();
1646
+
1647
+ try {
1648
+ const pending = await Promise.all(
1649
+ clipboardFiles.map(async (file) => {
1650
+ const dataBase64 = await readFileAsBase64(file);
1651
+ return {
1652
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1653
+ name: file.name || `paste-${Date.now()}`,
1654
+ size: file.size,
1655
+ mimeType: file.type || undefined,
1656
+ dataBase64,
1657
+ } satisfies PendingAttachment;
1658
+ })
1659
+ );
1660
+
1661
+ appendPendingAttachments(pending);
1662
+ } catch (error) {
1663
+ console.error('Failed to handle pasted files:', error);
1664
+ reportAttachmentError('Failed to attach pasted files.');
1665
+ }
1666
+ };
1667
+
1668
+ const renderAttachmentPanel = () => {
1669
+ if (pendingAttachments.length === 0 && !attachmentError) return null;
1670
+ return (
1671
+ <div className="attachment-panel">
1672
+ {attachmentError && <div className="attachment-error">{attachmentError}</div>}
1673
+ {pendingAttachments.length > 0 && (
1674
+ <div className="attachment-list">
1675
+ {pendingAttachments.map((attachment) => (
1676
+ <div className="attachment-chip" key={attachment.id}>
1677
+ <span className="attachment-icon">
1678
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1679
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
1680
+ <path d="M14 2v6h6" />
1681
+ </svg>
1682
+ </span>
1683
+ <span className="attachment-name" title={attachment.name}>{attachment.name}</span>
1684
+ <span className="attachment-size">{formatFileSize(attachment.size)}</span>
1685
+ <button
1686
+ className="attachment-remove"
1687
+ onClick={() => handleRemoveAttachment(attachment.id)}
1688
+ title="Remove attachment"
1689
+ disabled={isUploadingAttachments}
1690
+ >
1691
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1692
+ <path d="M18 6L6 18M6 6l12 12" />
1693
+ </svg>
1694
+ </button>
1695
+ </div>
1696
+ ))}
1697
+ </div>
1698
+ )}
1699
+ </div>
1700
+ );
1701
+ };
1702
+
1703
+ const importAttachmentsToWorkspace = async (): Promise<ImportedAttachment[]> => {
1704
+ if (pendingAttachments.length === 0) return [];
1705
+ if (!workspace) {
1706
+ throw new Error('Select a workspace before attaching files.');
1707
+ }
1708
+ const pathAttachments = pendingAttachments.filter((attachment) => attachment.path && !attachment.dataBase64);
1709
+ const dataAttachments = pendingAttachments.filter((attachment) => attachment.dataBase64);
1710
+
1711
+ const results: ImportedAttachment[] = [];
1712
+
1713
+ if (pathAttachments.length > 0) {
1714
+ const imported = await window.electronAPI.importFilesToWorkspace({
1715
+ workspaceId: workspace.id,
1716
+ files: pathAttachments.map((attachment) => attachment.path as string),
1717
+ });
1718
+ results.push(...imported);
1719
+ }
1720
+
1721
+ if (dataAttachments.length > 0) {
1722
+ const imported = await window.electronAPI.importDataToWorkspace({
1723
+ workspaceId: workspace.id,
1724
+ files: dataAttachments.map((attachment) => ({
1725
+ name: attachment.name,
1726
+ data: attachment.dataBase64 as string,
1727
+ mimeType: attachment.mimeType,
1728
+ })),
1729
+ });
1730
+ results.push(...imported);
1731
+ }
1732
+
1733
+ return results;
1734
+ };
1735
+
1736
+ const handleSend = async () => {
1737
+ const trimmedInput = inputValue.trim();
1738
+ const hasAttachments = pendingAttachments.length > 0;
1739
+
1740
+ if (!trimmedInput && !hasAttachments) return;
1741
+
1742
+ let importedAttachments: ImportedAttachment[] = [];
1743
+
1744
+ if (hasAttachments) {
1745
+ setIsUploadingAttachments(true);
1746
+ try {
1747
+ importedAttachments = await importAttachmentsToWorkspace();
1748
+ } catch (error) {
1749
+ console.error('Failed to import attachments:', error);
1750
+ reportAttachmentError(error instanceof Error ? error.message : 'Failed to upload attachments.');
1751
+ setIsUploadingAttachments(false);
1752
+ return;
1753
+ } finally {
1754
+ setIsUploadingAttachments(false);
1755
+ }
1756
+ }
1757
+
1758
+ const message = composeMessageWithAttachments(trimmedInput, importedAttachments);
1759
+
1760
+ // Use selectedTaskId to determine if we should follow-up or create new task
1761
+ // This fixes the bug where old tasks (beyond the 100 most recent) would create new tasks
1762
+ // instead of sending follow-up messages
1763
+ if (!selectedTaskId && onCreateTask) {
1764
+ // No task selected - create new task with optional Goal Mode options
1765
+ const titleSource = trimmedInput || (pendingAttachments[0]?.name ? `Review ${pendingAttachments[0].name}` : 'New task');
1766
+ const title = buildTaskTitle(titleSource);
1767
+ const options: GoalModeOptions | undefined = goalModeEnabled && verificationCommand
1768
+ ? {
1769
+ successCriteria: { type: 'shell_command' as const, command: verificationCommand },
1770
+ maxAttempts,
1771
+ }
1772
+ : undefined;
1773
+ onCreateTask(title, message, options);
1774
+ // Reset Goal Mode state
1775
+ setGoalModeEnabled(false);
1776
+ setVerificationCommand('');
1777
+ setMaxAttempts(3);
1778
+ } else {
1779
+ // Task is selected (even if not in current list) - send follow-up message
1780
+ onSendMessage(message);
1781
+ }
1782
+
1783
+ setInputValue('');
1784
+ setPendingAttachments([]);
1785
+ setAttachmentError(null);
1786
+ setMentionOpen(false);
1787
+ setMentionQuery('');
1788
+ setMentionTarget(null);
1789
+ };
1790
+
1254
1791
  const handleClearQueue = () => {
1255
1792
  setQueuedMessage(null);
1256
1793
  };
@@ -1402,7 +1939,10 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1402
1939
  className="mention-autocomplete-icon"
1403
1940
  style={{ backgroundColor: option.color || '#64748b' }}
1404
1941
  >
1405
- {option.icon || '👥'}
1942
+ <ThemeIcon
1943
+ emoji={option.icon || '👥'}
1944
+ icon={<UsersIcon size={16} />}
1945
+ />
1406
1946
  </span>
1407
1947
  <div className="mention-autocomplete-details">
1408
1948
  <span className="mention-autocomplete-name">{displayLabel}</span>
@@ -1442,7 +1982,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1442
1982
 
1443
1983
  if (e.key === 'Enter' && !e.shiftKey) {
1444
1984
  e.preventDefault();
1445
- handleSend();
1985
+ void handleSend();
1446
1986
  }
1447
1987
  };
1448
1988
 
@@ -1477,35 +2017,48 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1477
2017
  <div className="main-body welcome-view">
1478
2018
  <div className="welcome-content cli-style">
1479
2019
  {/* Logo */}
1480
- <div className="welcome-logo">
1481
- <img src="./cowork-os-logo.png" alt="CoWork OS" className="welcome-logo-img" />
2020
+ <div className="welcome-header-modern modern-only">
2021
+ <div className="modern-logo-container">
2022
+ <img src="./cowork-os-logo.png" alt="CoWork OS" className="modern-logo" />
2023
+ <div className="modern-title-container">
2024
+ <h1 className="modern-title">CoWork OS</h1>
2025
+ <span className="modern-version">{appVersion ? `v${appVersion}` : ''}</span>
2026
+ </div>
2027
+ </div>
2028
+ <p className="modern-subtitle">{agentContext.getMessage('welcomeSubtitle')}</p>
1482
2029
  </div>
1483
2030
 
1484
- {/* ASCII Terminal Header */}
1485
- <div className="cli-header">
1486
- <pre className="ascii-art">{`
2031
+ <div className="terminal-only">
2032
+ <div className="welcome-logo">
2033
+ <img src="./cowork-os-logo.png" alt="CoWork OS" className="welcome-logo-img" />
2034
+ </div>
2035
+
2036
+ {/* ASCII Terminal Header */}
2037
+ <div className="cli-header">
2038
+ <pre className="ascii-art">{`
1487
2039
  ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ███████╗
1488
2040
  ██╔════╝██╔═══██╗██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔════╝
1489
2041
  ██║ ██║ ██║██║ █╗ ██║██║ ██║██████╔╝█████╔╝ ██║ ██║███████╗
1490
2042
  ██║ ██║ ██║██║███╗██║██║ ██║██╔══██╗██╔═██╗ ██║ ██║╚════██║
1491
2043
  ╚██████╗╚██████╔╝╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝███████║
1492
2044
  ╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝`}</pre>
1493
- <div className="cli-version">{appVersion ? `v${appVersion}` : ''}</div>
1494
- </div>
1495
-
1496
- {/* Terminal Info */}
1497
- <div className="cli-info">
1498
- <div className="cli-line">
1499
- <span className="cli-prompt">$</span>
1500
- <span className="cli-text" title={agentContext.getMessage('welcome')}>{agentContext.getMessage('welcome')}</span>
2045
+ <div className="cli-version">{appVersion ? `v${appVersion}` : ''}</div>
1501
2046
  </div>
1502
- <div className="cli-line cli-line-secondary">
1503
- <span className="cli-prompt">&gt;</span>
1504
- <span className="cli-text">{agentContext.getMessage('welcomeSubtitle')}</span>
1505
- </div>
1506
- <div className="cli-line cli-line-disclosure">
1507
- <span className="cli-prompt">#</span>
1508
- <span className="cli-text cli-text-muted" title={agentContext.getMessage('disclaimer')}>{agentContext.getMessage('disclaimer')}</span>
2047
+
2048
+ {/* Terminal Info */}
2049
+ <div className="cli-info">
2050
+ <div className="cli-line">
2051
+ <span className="cli-prompt">$</span>
2052
+ <span className="cli-text" title={agentContext.getMessage('welcome')}>{agentContext.getMessage('welcome')}</span>
2053
+ </div>
2054
+ <div className="cli-line cli-line-secondary">
2055
+ <span className="cli-prompt">&gt;</span>
2056
+ <span className="cli-text">{agentContext.getMessage('welcomeSubtitle')}</span>
2057
+ </div>
2058
+ <div className="cli-line cli-line-disclosure">
2059
+ <span className="cli-prompt">#</span>
2060
+ <span className="cli-text cli-text-muted" title={agentContext.getMessage('disclaimer')}>{agentContext.getMessage('disclaimer')}</span>
2061
+ </div>
1509
2062
  </div>
1510
2063
  </div>
1511
2064
 
@@ -1513,36 +2066,37 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1513
2066
  <div className="cli-commands">
1514
2067
  <div className="cli-commands-header">
1515
2068
  <span className="cli-prompt">&gt;</span>
1516
- <span>QUICK START</span>
2069
+ <span className="terminal-only">QUICK START</span>
2070
+ <span className="modern-only">Quick start</span>
1517
2071
  </div>
1518
2072
  <div className="quick-start-grid">
1519
2073
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s organize the files in this folder together. Sort them by type and rename them with clear, consistent names.')} title="Let's sort and tidy up the workspace">
1520
- <span className="quick-start-icon">📁</span>
2074
+ <ThemeIcon className="quick-start-icon" emoji="📁" icon={<FolderIcon size={22} />} />
1521
2075
  <span className="quick-start-title">Organize files</span>
1522
2076
  <span className="quick-start-desc">Let's sort and tidy up the workspace</span>
1523
2077
  </button>
1524
2078
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s write a document together. I\'ll describe what I need and we can create it.')} title="Co-create reports, summaries, or notes">
1525
- <span className="quick-start-icon">📝</span>
2079
+ <ThemeIcon className="quick-start-icon" emoji="📝" icon={<EditIcon size={22} />} />
1526
2080
  <span className="quick-start-title">Write together</span>
1527
2081
  <span className="quick-start-desc">Co-create reports, summaries, or notes</span>
1528
2082
  </button>
1529
2083
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s analyze the data files in this folder together. We\'ll summarize the key findings and create a report.')} title="Work through spreadsheets or data files">
1530
- <span className="quick-start-icon">📊</span>
2084
+ <ThemeIcon className="quick-start-icon" emoji="📊" icon={<ChartIcon size={22} />} />
1531
2085
  <span className="quick-start-title">Analyze data</span>
1532
2086
  <span className="quick-start-desc">Work through spreadsheets or data files</span>
1533
2087
  </button>
1534
2088
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s generate documentation for this project together. We can create a README, API docs, or code comments as needed.')} title="Build documentation for the project">
1535
- <span className="quick-start-icon">📖</span>
2089
+ <ThemeIcon className="quick-start-icon" emoji="📖" icon={<BookIcon size={22} />} />
1536
2090
  <span className="quick-start-title">Generate docs</span>
1537
2091
  <span className="quick-start-desc">Build documentation for the project</span>
1538
2092
  </button>
1539
2093
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s research and summarize information from the files in this folder together.')} title="Dig through files and find insights">
1540
- <span className="quick-start-icon">🔍</span>
2094
+ <ThemeIcon className="quick-start-icon" emoji="🔍" icon={<SearchIcon size={22} />} />
1541
2095
  <span className="quick-start-title">Research together</span>
1542
2096
  <span className="quick-start-desc">Dig through files and find insights</span>
1543
2097
  </button>
1544
2098
  <button className="quick-start-card" onClick={() => handleQuickAction('Let\'s prepare for a meeting together. We\'ll create an agenda, talking points, and organize materials needed.')} title="Get everything ready for a clean meeting">
1545
- <span className="quick-start-icon">📋</span>
2099
+ <ThemeIcon className="quick-start-icon" emoji="📋" icon={<ClipboardIcon size={22} />} />
1546
2100
  <span className="quick-start-title">Meeting prep</span>
1547
2101
  <span className="quick-start-desc">Get everything ready for a clean meeting</span>
1548
2102
  </button>
@@ -1550,7 +2104,14 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1550
2104
  </div>
1551
2105
 
1552
2106
  {/* Input Area */}
1553
- <div className="welcome-input-container cli-input-container">
2107
+ {renderAttachmentPanel()}
2108
+ <div
2109
+ className={`welcome-input-container cli-input-container ${isDraggingFiles ? 'drag-over' : ''}`}
2110
+ onDragOver={handleDragOver}
2111
+ onDragEnter={handleDragOver}
2112
+ onDragLeave={handleDragLeave}
2113
+ onDrop={handleDrop}
2114
+ >
1554
2115
  {showVoiceNotConfigured && (
1555
2116
  <div className="voice-not-configured-banner">
1556
2117
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -1591,6 +2152,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1591
2152
  value={inputValue}
1592
2153
  onChange={handleInputChange}
1593
2154
  onKeyDown={handleKeyDown}
2155
+ onPaste={handlePaste}
1594
2156
  onClick={handleInputClick}
1595
2157
  onKeyUp={handleInputKeyUp}
1596
2158
  rows={1}
@@ -1641,6 +2203,25 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1641
2203
 
1642
2204
  <div className="welcome-input-footer">
1643
2205
  <div className="input-left-actions">
2206
+ <button
2207
+ className="attachment-btn attachment-btn-left"
2208
+ onClick={handleAttachFiles}
2209
+ disabled={isUploadingAttachments}
2210
+ title="Attach files"
2211
+ >
2212
+ <svg
2213
+ width="18"
2214
+ height="18"
2215
+ viewBox="0 0 24 24"
2216
+ fill="none"
2217
+ stroke="currentColor"
2218
+ strokeWidth="2"
2219
+ strokeLinecap="round"
2220
+ strokeLinejoin="round"
2221
+ >
2222
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
2223
+ </svg>
2224
+ </button>
1644
2225
  <div className="workspace-dropdown-container" ref={workspaceDropdownRef}>
1645
2226
  <button className="folder-selector" onClick={handleWorkspaceDropdownToggle}>
1646
2227
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -1657,7 +2238,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1657
2238
  <>
1658
2239
  <div className="workspace-dropdown-header">Recent Folders</div>
1659
2240
  <div className="workspace-dropdown-list">
1660
- {workspacesList.slice(0, 5).map((w) => (
2241
+ {workspacesList.slice(0, 10).map((w) => (
1661
2242
  <button
1662
2243
  key={w.id}
1663
2244
  className={`workspace-dropdown-item ${workspace?.id === w.id ? 'active' : ''}`}
@@ -1706,6 +2287,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1706
2287
  models={availableModels}
1707
2288
  selectedModel={selectedModel}
1708
2289
  onModelChange={onModelChange}
2290
+ onOpenSettings={onOpenSettings}
1709
2291
  />
1710
2292
  {/* Skills Menu Button */}
1711
2293
  <div className="skills-menu-container" ref={skillsMenuRef}>
@@ -1785,8 +2367,8 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1785
2367
  disabled={voiceInput.state === 'processing'}
1786
2368
  title={
1787
2369
  voiceInput.state === 'idle' ? 'Start voice input' :
1788
- voiceInput.state === 'recording' ? 'Stop recording' :
1789
- 'Processing...'
2370
+ voiceInput.state === 'recording' ? 'Stop recording' :
2371
+ 'Processing...'
1790
2372
  }
1791
2373
  >
1792
2374
  {voiceInput.state === 'processing' ? (
@@ -1813,7 +2395,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1813
2395
  <button
1814
2396
  className="lets-go-btn lets-go-btn-sm"
1815
2397
  onClick={handleSend}
1816
- disabled={!inputValue.trim()}
2398
+ disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isUploadingAttachments}
1817
2399
  >
1818
2400
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1819
2401
  <path d="M12 19V5M5 12l7-7 7 7" />
@@ -1925,6 +2507,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1925
2507
  session={item.session}
1926
2508
  onClose={() => handleCanvasClose(item.session.id)}
1927
2509
  forceSnapshot={item.forceSnapshot}
2510
+ onOpenBrowser={onOpenBrowserView}
1928
2511
  />
1929
2512
  );
1930
2513
  }
@@ -2056,7 +2639,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2056
2639
  </div>
2057
2640
  <div className="event-time">{formatTime(event.timestamp)}</div>
2058
2641
  </div>
2059
- {isExpanded && renderEventDetails(event, voiceEnabled)}
2642
+ {isExpanded && renderEventDetails(event, voiceEnabled, markdownComponents)}
2060
2643
  </div>
2061
2644
  </div>
2062
2645
  {shouldRenderCommandOutput && (
@@ -2080,7 +2663,14 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2080
2663
 
2081
2664
  {/* Footer with Input */}
2082
2665
  <div className="main-footer">
2083
- <div className="input-container">
2666
+ {renderAttachmentPanel()}
2667
+ <div
2668
+ className={`input-container ${isDraggingFiles ? 'drag-over' : ''}`}
2669
+ onDragOver={handleDragOver}
2670
+ onDragEnter={handleDragOver}
2671
+ onDragLeave={handleDragLeave}
2672
+ onDrop={handleDrop}
2673
+ >
2084
2674
  {/* Queued message display */}
2085
2675
  {queuedMessage && (
2086
2676
  <div className="queued-message-frame">
@@ -2151,6 +2741,25 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2151
2741
  </div>
2152
2742
  )}
2153
2743
  <div className="input-row">
2744
+ <button
2745
+ className="attachment-btn attachment-btn-left"
2746
+ onClick={handleAttachFiles}
2747
+ disabled={isUploadingAttachments}
2748
+ title="Attach files"
2749
+ >
2750
+ <svg
2751
+ width="18"
2752
+ height="18"
2753
+ viewBox="0 0 24 24"
2754
+ fill="none"
2755
+ stroke="currentColor"
2756
+ strokeWidth="2"
2757
+ strokeLinecap="round"
2758
+ strokeLinejoin="round"
2759
+ >
2760
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
2761
+ </svg>
2762
+ </button>
2154
2763
  <div className="mention-autocomplete-wrapper" ref={mentionContainerRef}>
2155
2764
  <textarea
2156
2765
  ref={textareaRef}
@@ -2159,6 +2768,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2159
2768
  value={inputValue}
2160
2769
  onChange={handleInputChange}
2161
2770
  onKeyDown={handleKeyDown}
2771
+ onPaste={handlePaste}
2162
2772
  onClick={handleInputClick}
2163
2773
  onKeyUp={handleInputKeyUp}
2164
2774
  rows={1}
@@ -2170,6 +2780,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2170
2780
  models={availableModels}
2171
2781
  selectedModel={selectedModel}
2172
2782
  onModelChange={onModelChange}
2783
+ onOpenSettings={onOpenSettings}
2173
2784
  />
2174
2785
  <button
2175
2786
  className={`voice-input-btn ${voiceInput.state}`}
@@ -2177,8 +2788,8 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2177
2788
  disabled={voiceInput.state === 'processing'}
2178
2789
  title={
2179
2790
  voiceInput.state === 'idle' ? 'Start voice input' :
2180
- voiceInput.state === 'recording' ? 'Stop recording' :
2181
- 'Processing...'
2791
+ voiceInput.state === 'recording' ? 'Stop recording' :
2792
+ 'Processing...'
2182
2793
  }
2183
2794
  >
2184
2795
  {voiceInput.state === 'processing' ? (
@@ -2205,7 +2816,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2205
2816
  <button
2206
2817
  className="lets-go-btn lets-go-btn-sm"
2207
2818
  onClick={handleSend}
2208
- disabled={!inputValue.trim()}
2819
+ disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isUploadingAttachments}
2209
2820
  title="Send message"
2210
2821
  >
2211
2822
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -2226,6 +2837,59 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2226
2837
  </div>
2227
2838
  </div>
2228
2839
  <div className="input-below-actions">
2840
+ <div className="workspace-dropdown-container" ref={workspaceDropdownRef}>
2841
+ <button
2842
+ className="folder-selector"
2843
+ onClick={handleWorkspaceDropdownToggle}
2844
+ title={workspace?.path || 'Select a workspace folder'}
2845
+ >
2846
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2847
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
2848
+ </svg>
2849
+ <span>{workspace?.id === TEMP_WORKSPACE_ID ? 'Work in a folder' : (workspace?.name || 'Work in a folder')}</span>
2850
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={showWorkspaceDropdown ? 'chevron-up' : ''}>
2851
+ <path d="M6 9l6 6 6-6" />
2852
+ </svg>
2853
+ </button>
2854
+ {showWorkspaceDropdown && (
2855
+ <div className="workspace-dropdown">
2856
+ {workspacesList.length > 0 && (
2857
+ <>
2858
+ <div className="workspace-dropdown-header">Recent Folders</div>
2859
+ <div className="workspace-dropdown-list">
2860
+ {workspacesList.slice(0, 10).map((w) => (
2861
+ <button
2862
+ key={w.id}
2863
+ className={`workspace-dropdown-item ${workspace?.id === w.id ? 'active' : ''}`}
2864
+ onClick={() => handleWorkspaceSelect(w)}
2865
+ >
2866
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2867
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
2868
+ </svg>
2869
+ <div className="workspace-item-info">
2870
+ <span className="workspace-item-name">{w.name}</span>
2871
+ <span className="workspace-item-path">{w.path}</span>
2872
+ </div>
2873
+ {workspace?.id === w.id && (
2874
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="check-icon">
2875
+ <path d="M20 6L9 17l-5-5" />
2876
+ </svg>
2877
+ )}
2878
+ </button>
2879
+ ))}
2880
+ </div>
2881
+ <div className="workspace-dropdown-divider" />
2882
+ </>
2883
+ )}
2884
+ <button className="workspace-dropdown-item new-folder" onClick={handleSelectNewFolder}>
2885
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2886
+ <path d="M12 5v14M5 12h14" />
2887
+ </svg>
2888
+ <span>Work in another folder...</span>
2889
+ </button>
2890
+ </div>
2891
+ )}
2892
+ </div>
2229
2893
  <button
2230
2894
  className={`shell-toggle ${shellEnabled ? 'enabled' : ''}`}
2231
2895
  onClick={handleShellToggle}
@@ -2380,7 +3044,7 @@ function renderEventTitle(
2380
3044
  }
2381
3045
  }
2382
3046
 
2383
- function renderEventDetails(event: TaskEvent, voiceEnabled: boolean) {
3047
+ function renderEventDetails(event: TaskEvent, voiceEnabled: boolean, markdownComponents: any) {
2384
3048
  switch (event.type) {
2385
3049
  case 'plan_created':
2386
3050
  return (