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.
- package/README.md +372 -10
- package/connectors/README.md +20 -0
- package/connectors/asana-mcp/README.md +24 -0
- package/connectors/asana-mcp/dist/index.js +427 -0
- package/connectors/asana-mcp/package.json +15 -0
- package/connectors/asana-mcp/src/index.ts +553 -0
- package/connectors/asana-mcp/tsconfig.json +13 -0
- package/connectors/hubspot-mcp/README.md +35 -0
- package/connectors/hubspot-mcp/dist/index.js +454 -0
- package/connectors/hubspot-mcp/package.json +15 -0
- package/connectors/hubspot-mcp/src/index.ts +562 -0
- package/connectors/hubspot-mcp/tsconfig.json +13 -0
- package/connectors/jira-mcp/README.md +49 -0
- package/connectors/jira-mcp/dist/index.js +588 -0
- package/connectors/jira-mcp/package.json +15 -0
- package/connectors/jira-mcp/src/index.ts +711 -0
- package/connectors/jira-mcp/tsconfig.json +13 -0
- package/connectors/linear-mcp/README.md +22 -0
- package/connectors/linear-mcp/dist/index.js +402 -0
- package/connectors/linear-mcp/package.json +15 -0
- package/connectors/linear-mcp/src/index.ts +522 -0
- package/connectors/linear-mcp/tsconfig.json +13 -0
- package/connectors/okta-mcp/README.md +24 -0
- package/connectors/okta-mcp/dist/index.js +411 -0
- package/connectors/okta-mcp/package.json +15 -0
- package/connectors/okta-mcp/src/index.ts +520 -0
- package/connectors/okta-mcp/tsconfig.json +13 -0
- package/connectors/salesforce-mcp/README.md +47 -0
- package/connectors/salesforce-mcp/dist/index.js +584 -0
- package/connectors/salesforce-mcp/package.json +15 -0
- package/connectors/salesforce-mcp/src/index.ts +722 -0
- package/connectors/salesforce-mcp/tsconfig.json +13 -0
- package/connectors/servicenow-mcp/README.md +26 -0
- package/connectors/servicenow-mcp/dist/index.js +400 -0
- package/connectors/servicenow-mcp/package.json +15 -0
- package/connectors/servicenow-mcp/src/index.ts +500 -0
- package/connectors/servicenow-mcp/tsconfig.json +13 -0
- package/connectors/templates/mcp-connector/README.md +31 -0
- package/connectors/templates/mcp-connector/package.json +15 -0
- package/connectors/templates/mcp-connector/src/index.ts +330 -0
- package/connectors/templates/mcp-connector/tsconfig.json +13 -0
- package/connectors/zendesk-mcp/README.md +40 -0
- package/connectors/zendesk-mcp/dist/index.js +431 -0
- package/connectors/zendesk-mcp/package.json +15 -0
- package/connectors/zendesk-mcp/src/index.ts +543 -0
- package/connectors/zendesk-mcp/tsconfig.json +13 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
- package/dist/electron/electron/agent/daemon.js +189 -13
- package/dist/electron/electron/agent/executor.js +895 -78
- package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
- package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
- package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
- package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
- package/dist/electron/electron/agent/llm/index.js +13 -1
- package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
- package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
- package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
- package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
- package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
- package/dist/electron/electron/agent/llm/types.js +66 -1
- package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
- package/dist/electron/electron/agent/search/provider-factory.js +38 -2
- package/dist/electron/electron/agent/tools/box-tools.js +231 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
- package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
- package/dist/electron/electron/agent/tools/file-tools.js +66 -3
- package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
- package/dist/electron/electron/agent/tools/image-tools.js +11 -1
- package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
- package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
- package/dist/electron/electron/agent/tools/registry.js +548 -10
- package/dist/electron/electron/agent/tools/search-tools.js +28 -10
- package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
- package/dist/electron/electron/agent/tools/x-tools.js +1 -1
- package/dist/electron/electron/agents/agent-dispatch.js +63 -0
- package/dist/electron/electron/database/repositories.js +19 -5
- package/dist/electron/electron/database/schema.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
- package/dist/electron/electron/gateway/index.js +75 -1
- package/dist/electron/electron/gateway/router.js +209 -154
- package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
- package/dist/electron/electron/ipc/handlers.js +763 -267
- package/dist/electron/electron/main.js +63 -0
- package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
- package/dist/electron/electron/memory/MemoryService.js +2 -1
- package/dist/electron/electron/preload.js +78 -1
- package/dist/electron/electron/settings/appearance-manager.js +18 -1
- package/dist/electron/electron/settings/box-manager.js +54 -0
- package/dist/electron/electron/settings/dropbox-manager.js +54 -0
- package/dist/electron/electron/settings/google-drive-manager.js +54 -0
- package/dist/electron/electron/settings/notion-manager.js +56 -0
- package/dist/electron/electron/settings/onedrive-manager.js +54 -0
- package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
- package/dist/electron/electron/utils/box-api.js +153 -0
- package/dist/electron/electron/utils/dropbox-api.js +144 -0
- package/dist/electron/electron/utils/env-migration.js +19 -0
- package/dist/electron/electron/utils/google-drive-api.js +152 -0
- package/dist/electron/electron/utils/notion-api.js +103 -0
- package/dist/electron/electron/utils/onedrive-api.js +113 -0
- package/dist/electron/electron/utils/sharepoint-api.js +109 -0
- package/dist/electron/electron/utils/validation.js +98 -3
- package/dist/electron/electron/utils/x-cli.js +1 -1
- package/dist/electron/shared/channelMessages.js +284 -3
- package/dist/electron/shared/llm-provider-catalog.js +198 -0
- package/dist/electron/shared/types.js +90 -1
- package/package.json +14 -3
- package/resources/skills/nano-banana-pro.json +4 -4
- package/resources/skills/openai-image-gen.json +3 -3
- package/resources/skills/scripts/gen.py +163 -0
- package/resources/skills/scripts/generate_image.py +91 -0
- package/src/electron/agent/custom-skill-loader.ts +34 -1
- package/src/electron/agent/daemon.ts +210 -14
- package/src/electron/agent/executor.ts +1124 -85
- package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
- package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
- package/src/electron/agent/llm/bedrock-provider.ts +62 -9
- package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
- package/src/electron/agent/llm/groq-provider.ts +39 -0
- package/src/electron/agent/llm/index.ts +6 -0
- package/src/electron/agent/llm/kimi-provider.ts +39 -0
- package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
- package/src/electron/agent/llm/openai-compatible.ts +133 -0
- package/src/electron/agent/llm/openai-oauth.ts +2 -1
- package/src/electron/agent/llm/openrouter-provider.ts +2 -1
- package/src/electron/agent/llm/provider-factory.ts +459 -6
- package/src/electron/agent/llm/types.ts +95 -1
- package/src/electron/agent/llm/xai-provider.ts +39 -0
- package/src/electron/agent/search/provider-factory.ts +43 -2
- package/src/electron/agent/tools/box-tools.ts +239 -0
- package/src/electron/agent/tools/builtin-settings.ts +36 -0
- package/src/electron/agent/tools/dropbox-tools.ts +237 -0
- package/src/electron/agent/tools/file-tools.ts +66 -3
- package/src/electron/agent/tools/gmail-tools.ts +240 -0
- package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
- package/src/electron/agent/tools/google-drive-tools.ts +228 -0
- package/src/electron/agent/tools/grep-tools.ts +97 -12
- package/src/electron/agent/tools/image-tools.ts +11 -1
- package/src/electron/agent/tools/notion-tools.ts +330 -0
- package/src/electron/agent/tools/onedrive-tools.ts +217 -0
- package/src/electron/agent/tools/registry.ts +794 -10
- package/src/electron/agent/tools/search-tools.ts +29 -11
- package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
- package/src/electron/agent/tools/shell-tools.ts +11 -3
- package/src/electron/agent/tools/x-tools.ts +1 -1
- package/src/electron/agents/agent-dispatch.ts +79 -0
- package/src/electron/database/SecureSettingsRepository.ts +7 -1
- package/src/electron/database/repositories.ts +58 -6
- package/src/electron/database/schema.ts +8 -0
- package/src/electron/gateway/channels/discord.ts +4 -0
- package/src/electron/gateway/channels/google-chat.ts +3 -0
- package/src/electron/gateway/channels/line.ts +3 -0
- package/src/electron/gateway/channels/matrix-client.ts +15 -0
- package/src/electron/gateway/channels/matrix.ts +31 -0
- package/src/electron/gateway/channels/mattermost.ts +3 -0
- package/src/electron/gateway/channels/signal.ts +3 -0
- package/src/electron/gateway/channels/slack.ts +9 -4
- package/src/electron/gateway/channels/teams.ts +4 -0
- package/src/electron/gateway/channels/telegram.ts +2 -0
- package/src/electron/gateway/channels/twitch.ts +2 -0
- package/src/electron/gateway/channels/types.ts +8 -0
- package/src/electron/gateway/channels/whatsapp.ts +66 -0
- package/src/electron/gateway/index.ts +95 -2
- package/src/electron/gateway/router.ts +231 -161
- package/src/electron/gateway/security.ts +21 -9
- package/src/electron/ipc/canvas-handlers.ts +10 -0
- package/src/electron/ipc/handlers.ts +848 -292
- package/src/electron/main.ts +35 -0
- package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
- package/src/electron/memory/MemoryService.ts +7 -1
- package/src/electron/preload.ts +200 -5
- package/src/electron/settings/appearance-manager.ts +20 -2
- package/src/electron/settings/box-manager.ts +58 -0
- package/src/electron/settings/dropbox-manager.ts +58 -0
- package/src/electron/settings/google-workspace-manager.ts +59 -0
- package/src/electron/settings/notion-manager.ts +60 -0
- package/src/electron/settings/onedrive-manager.ts +58 -0
- package/src/electron/settings/sharepoint-manager.ts +58 -0
- package/src/electron/utils/box-api.ts +184 -0
- package/src/electron/utils/dropbox-api.ts +171 -0
- package/src/electron/utils/env-migration.ts +22 -0
- package/src/electron/utils/gmail-api.ts +121 -0
- package/src/electron/utils/google-calendar-api.ts +115 -0
- package/src/electron/utils/google-workspace-api.ts +228 -0
- package/src/electron/utils/google-workspace-auth.ts +109 -0
- package/src/electron/utils/google-workspace-oauth.ts +232 -0
- package/src/electron/utils/notion-api.ts +126 -0
- package/src/electron/utils/onedrive-api.ts +137 -0
- package/src/electron/utils/sharepoint-api.ts +132 -0
- package/src/electron/utils/validation.ts +128 -1
- package/src/electron/utils/x-cli.ts +1 -1
- package/src/renderer/App.tsx +119 -8
- package/src/renderer/components/ActivityFeedItem.tsx +34 -17
- package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
- package/src/renderer/components/AppearanceSettings.tsx +37 -2
- package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
- package/src/renderer/components/BoxSettings.tsx +203 -0
- package/src/renderer/components/BrowserView.tsx +101 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
- package/src/renderer/components/CanvasPreview.tsx +68 -1
- package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
- package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
- package/src/renderer/components/ConnectorsSettings.tsx +397 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
- package/src/renderer/components/DiscordSettings.tsx +18 -7
- package/src/renderer/components/DropboxSettings.tsx +202 -0
- package/src/renderer/components/EmailSettings.tsx +18 -7
- package/src/renderer/components/FileViewer.tsx +21 -13
- package/src/renderer/components/GoogleChatSettings.tsx +17 -7
- package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
- package/src/renderer/components/ImessageSettings.tsx +22 -11
- package/src/renderer/components/LineIcons.tsx +376 -0
- package/src/renderer/components/LineSettings.tsx +18 -7
- package/src/renderer/components/MCPSettings.tsx +56 -0
- package/src/renderer/components/MainContent.tsx +740 -76
- package/src/renderer/components/MatrixSettings.tsx +18 -7
- package/src/renderer/components/MattermostSettings.tsx +18 -7
- package/src/renderer/components/NodesSettings.tsx +58 -99
- package/src/renderer/components/NotificationPanel.tsx +25 -11
- package/src/renderer/components/NotionSettings.tsx +231 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
- package/src/renderer/components/OnboardingModal.tsx +70 -1
- package/src/renderer/components/OneDriveSettings.tsx +212 -0
- package/src/renderer/components/RightPanel.tsx +141 -28
- package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
- package/src/renderer/components/SearchSettings.tsx +118 -114
- package/src/renderer/components/Settings.tsx +1425 -651
- package/src/renderer/components/SharePointSettings.tsx +224 -0
- package/src/renderer/components/Sidebar.tsx +94 -19
- package/src/renderer/components/SignalSettings.tsx +18 -7
- package/src/renderer/components/SkillHubBrowser.tsx +144 -185
- package/src/renderer/components/SlackSettings.tsx +18 -7
- package/src/renderer/components/TaskQuickActions.tsx +11 -6
- package/src/renderer/components/TaskTimeline.tsx +58 -26
- package/src/renderer/components/TeamsSettings.tsx +18 -7
- package/src/renderer/components/TelegramSettings.tsx +18 -7
- package/src/renderer/components/ThemeIcon.tsx +16 -0
- package/src/renderer/components/TwitchSettings.tsx +18 -7
- package/src/renderer/components/VoiceSettings.tsx +30 -74
- package/src/renderer/components/WhatsAppSettings.tsx +48 -37
- package/src/renderer/components/WorkingStateHistory.tsx +7 -5
- package/src/renderer/components/WorkspaceSelector.tsx +42 -13
- package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
- package/src/renderer/styles/index.css +2333 -209
- package/src/shared/channelMessages.ts +367 -4
- package/src/shared/llm-provider-catalog.ts +217 -0
- 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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
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-
|
|
1481
|
-
<
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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">></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">></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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2181
|
-
|
|
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 (
|