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