@agent-relay/dashboard 2.0.81 → 2.0.82
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/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpawnModal Component
|
|
3
|
+
*
|
|
4
|
+
* Modal for spawning new agent instances with configuration options.
|
|
5
|
+
* Supports different agent types (claude, codex, etc.) and naming conventions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
import { getAgentColor, getAgentInitials } from '../lib/colors';
|
|
10
|
+
import { cloudApi } from '../lib/cloudApi';
|
|
11
|
+
|
|
12
|
+
export type SpeakOnTrigger = 'SESSION_END' | 'CODE_WRITTEN' | 'REVIEW_REQUEST' | 'EXPLICIT_ASK' | 'ALL_MESSAGES';
|
|
13
|
+
|
|
14
|
+
export interface SpawnConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
command: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
team?: string;
|
|
19
|
+
shadowMode?: 'subagent' | 'process';
|
|
20
|
+
shadowOf?: string;
|
|
21
|
+
shadowAgent?: string;
|
|
22
|
+
shadowTriggers?: SpeakOnTrigger[];
|
|
23
|
+
shadowSpeakOn?: SpeakOnTrigger[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deriveShadowMode(command: string): 'subagent' | 'process' {
|
|
27
|
+
const base = command.trim().split(' ')[0].toLowerCase();
|
|
28
|
+
if (base.startsWith('claude') || base === 'codex' || base === 'opencode' || base === 'gemini' || base === 'droid' || base === 'cursor') return 'subagent';
|
|
29
|
+
return 'process';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SpawnModalProps {
|
|
33
|
+
isOpen: boolean;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
onSpawn: (config: SpawnConfig) => Promise<boolean>;
|
|
36
|
+
existingAgents: string[];
|
|
37
|
+
isSpawning?: boolean;
|
|
38
|
+
error?: string | null;
|
|
39
|
+
/** Whether running in cloud mode (enables credentials check) */
|
|
40
|
+
isCloudMode?: boolean;
|
|
41
|
+
/** Active workspace ID for provider setup redirect */
|
|
42
|
+
workspaceId?: string;
|
|
43
|
+
/** Agent defaults from settings */
|
|
44
|
+
agentDefaults?: {
|
|
45
|
+
defaultCliType: string | null;
|
|
46
|
+
defaultModels: {
|
|
47
|
+
claude: string;
|
|
48
|
+
cursor: string;
|
|
49
|
+
codex: string;
|
|
50
|
+
gemini: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/** Available workspace repos (cloud mode) */
|
|
54
|
+
repos?: Array<{ id: string; githubFullName: string }>;
|
|
55
|
+
/** Currently active repo ID (cloud mode) */
|
|
56
|
+
activeRepoId?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Model options for Claude agents */
|
|
60
|
+
export const CLAUDE_MODEL_OPTIONS: { value: string; label: string }[] = [
|
|
61
|
+
{ value: 'sonnet', label: 'Sonnet' },
|
|
62
|
+
{ value: 'opus', label: 'Opus' },
|
|
63
|
+
{ value: 'haiku', label: 'Haiku' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
type ClaudeModel = string;
|
|
67
|
+
|
|
68
|
+
/** Model options for Cursor agents */
|
|
69
|
+
export const CURSOR_MODEL_OPTIONS: { value: string; label: string }[] = [
|
|
70
|
+
{ value: 'opus-4.5-thinking', label: 'Claude 4.5 Opus (Thinking)' },
|
|
71
|
+
{ value: 'opus-4.5', label: 'Claude 4.5 Opus' },
|
|
72
|
+
{ value: 'sonnet-4.5', label: 'Claude 4.5 Sonnet' },
|
|
73
|
+
{ value: 'sonnet-4.5-thinking', label: 'Claude 4.5 Sonnet (Thinking)' },
|
|
74
|
+
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
|
75
|
+
{ value: 'gpt-5.2-codex-high', label: 'GPT-5.2 Codex High' },
|
|
76
|
+
{ value: 'gpt-5.2-codex-low', label: 'GPT-5.2 Codex Low' },
|
|
77
|
+
{ value: 'gpt-5.2-codex-xhigh', label: 'GPT-5.2 Codex Extra High' },
|
|
78
|
+
{ value: 'gpt-5.2-codex-fast', label: 'GPT-5.2 Codex Fast' },
|
|
79
|
+
{ value: 'gpt-5.2-codex-high-fast', label: 'GPT-5.2 Codex High Fast' },
|
|
80
|
+
{ value: 'gpt-5.2-codex-low-fast', label: 'GPT-5.2 Codex Low Fast' },
|
|
81
|
+
{ value: 'gpt-5.2-codex-xhigh-fast', label: 'GPT-5.2 Codex Extra High Fast' },
|
|
82
|
+
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
|
83
|
+
{ value: 'gpt-5.1-codex-max-high', label: 'GPT-5.1 Codex Max High' },
|
|
84
|
+
{ value: 'gpt-5.2', label: 'GPT-5.2' },
|
|
85
|
+
{ value: 'gpt-5.2-high', label: 'GPT-5.2 High' },
|
|
86
|
+
{ value: 'gpt-5.1-high', label: 'GPT-5.1 High' },
|
|
87
|
+
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro' },
|
|
88
|
+
{ value: 'gemini-3-flash', label: 'Gemini 3 Flash' },
|
|
89
|
+
{ value: 'composer-1', label: 'Composer 1' },
|
|
90
|
+
{ value: 'grok', label: 'Grok' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
type CursorModel = string;
|
|
94
|
+
|
|
95
|
+
/** Model options for Codex agents */
|
|
96
|
+
export const CODEX_MODEL_OPTIONS: { value: string; label: string }[] = [
|
|
97
|
+
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex — Frontier agentic coding model' },
|
|
98
|
+
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex — Latest frontier agentic coding model' },
|
|
99
|
+
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max — Deep and fast reasoning' },
|
|
100
|
+
{ value: 'gpt-5.2', label: 'GPT-5.2 — Frontier model, knowledge & reasoning' },
|
|
101
|
+
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini — Cheaper, faster' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
type CodexModel = string;
|
|
105
|
+
|
|
106
|
+
/** Model options for Gemini agents */
|
|
107
|
+
export const GEMINI_MODEL_OPTIONS: { value: string; label: string }[] = [
|
|
108
|
+
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
|
|
109
|
+
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
110
|
+
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
111
|
+
{ value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
type GeminiModel = string;
|
|
115
|
+
|
|
116
|
+
const AGENT_TEMPLATES = [
|
|
117
|
+
{
|
|
118
|
+
id: 'claude',
|
|
119
|
+
name: 'Claude',
|
|
120
|
+
command: 'claude',
|
|
121
|
+
description: 'Claude Code CLI agent',
|
|
122
|
+
icon: '🤖',
|
|
123
|
+
providerId: 'anthropic', // Maps to provider credential ID
|
|
124
|
+
supportsModelSelection: true,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'codex',
|
|
128
|
+
name: 'Codex',
|
|
129
|
+
command: 'codex',
|
|
130
|
+
description: 'OpenAI Codex agent',
|
|
131
|
+
icon: '⚡',
|
|
132
|
+
providerId: 'codex',
|
|
133
|
+
supportsModelSelection: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'gemini',
|
|
137
|
+
name: 'Gemini',
|
|
138
|
+
command: 'gemini',
|
|
139
|
+
description: 'Google Gemini CLI agent',
|
|
140
|
+
icon: '💎',
|
|
141
|
+
providerId: 'google',
|
|
142
|
+
supportsModelSelection: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'opencode',
|
|
146
|
+
name: 'OpenCode',
|
|
147
|
+
command: 'opencode',
|
|
148
|
+
description: 'OpenCode AI agent',
|
|
149
|
+
icon: '🔷',
|
|
150
|
+
providerId: 'opencode',
|
|
151
|
+
comingSoon: true, // Not yet fully tested
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'droid',
|
|
155
|
+
name: 'Droid',
|
|
156
|
+
command: 'droid',
|
|
157
|
+
description: 'Factory Droid agent',
|
|
158
|
+
icon: '🤖',
|
|
159
|
+
providerId: 'droid',
|
|
160
|
+
comingSoon: true, // Not yet fully tested
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'cursor',
|
|
164
|
+
name: 'Cursor',
|
|
165
|
+
command: 'cursor',
|
|
166
|
+
description: 'Cursor AI agent',
|
|
167
|
+
icon: '📝',
|
|
168
|
+
providerId: 'cursor',
|
|
169
|
+
supportsModelSelection: true,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'custom',
|
|
173
|
+
name: 'Custom',
|
|
174
|
+
command: '',
|
|
175
|
+
description: 'Custom command',
|
|
176
|
+
icon: '🔧',
|
|
177
|
+
providerId: null, // Custom commands don't require credentials check
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
export function SpawnModal({
|
|
182
|
+
isOpen,
|
|
183
|
+
onClose,
|
|
184
|
+
onSpawn,
|
|
185
|
+
existingAgents,
|
|
186
|
+
isSpawning = false,
|
|
187
|
+
error,
|
|
188
|
+
isCloudMode = false,
|
|
189
|
+
workspaceId,
|
|
190
|
+
agentDefaults,
|
|
191
|
+
repos,
|
|
192
|
+
activeRepoId,
|
|
193
|
+
}: SpawnModalProps) {
|
|
194
|
+
const [selectedTemplate, setSelectedTemplate] = useState(AGENT_TEMPLATES[0]);
|
|
195
|
+
const [name, setName] = useState('');
|
|
196
|
+
const [customCommand, setCustomCommand] = useState('');
|
|
197
|
+
const [selectedModel, setSelectedModel] = useState<ClaudeModel>('sonnet');
|
|
198
|
+
const [selectedCursorModel, setSelectedCursorModel] = useState<CursorModel>('opus-4.5-thinking');
|
|
199
|
+
const [selectedCodexModel, setSelectedCodexModel] = useState<CodexModel>('gpt-5.2-codex');
|
|
200
|
+
const [selectedGeminiModel, setSelectedGeminiModel] = useState<GeminiModel>('gemini-2.5-pro');
|
|
201
|
+
const [cwd, setCwd] = useState('');
|
|
202
|
+
const [selectedRepoId, setSelectedRepoId] = useState<string | undefined>(activeRepoId);
|
|
203
|
+
const [team, setTeam] = useState('');
|
|
204
|
+
const [isShadow, setIsShadow] = useState(false);
|
|
205
|
+
const [shadowOf, setShadowOf] = useState('');
|
|
206
|
+
const [shadowAgent, setShadowAgent] = useState('');
|
|
207
|
+
const [shadowSpeakOn, setShadowSpeakOn] = useState<SpeakOnTrigger[]>(['EXPLICIT_ASK']);
|
|
208
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
209
|
+
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
210
|
+
|
|
211
|
+
// Build effective command, always including model flag for Claude, Cursor, and Codex
|
|
212
|
+
const effectiveCommand = useMemo(() => {
|
|
213
|
+
if (selectedTemplate.id === 'custom') {
|
|
214
|
+
return customCommand;
|
|
215
|
+
}
|
|
216
|
+
// For Claude, always append model flag
|
|
217
|
+
if (selectedTemplate.id === 'claude') {
|
|
218
|
+
return `${selectedTemplate.command} --model ${selectedModel}`;
|
|
219
|
+
}
|
|
220
|
+
// For Cursor, always append model flag
|
|
221
|
+
if (selectedTemplate.id === 'cursor') {
|
|
222
|
+
return `${selectedTemplate.command} --model ${selectedCursorModel}`;
|
|
223
|
+
}
|
|
224
|
+
// For Codex, always append model flag
|
|
225
|
+
if (selectedTemplate.id === 'codex') {
|
|
226
|
+
return `${selectedTemplate.command} --model ${selectedCodexModel}`;
|
|
227
|
+
}
|
|
228
|
+
// For Gemini, always append model flag
|
|
229
|
+
if (selectedTemplate.id === 'gemini') {
|
|
230
|
+
return `${selectedTemplate.command} --model ${selectedGeminiModel}`;
|
|
231
|
+
}
|
|
232
|
+
return selectedTemplate.command;
|
|
233
|
+
}, [selectedTemplate, customCommand, selectedModel, selectedCursorModel, selectedCodexModel, selectedGeminiModel]);
|
|
234
|
+
|
|
235
|
+
const shadowMode = useMemo(() => deriveShadowMode(effectiveCommand), [effectiveCommand]);
|
|
236
|
+
|
|
237
|
+
// Provider credentials state (for cloud mode)
|
|
238
|
+
const [connectedProviders, setConnectedProviders] = useState<Set<string>>(new Set());
|
|
239
|
+
const [isLoadingCredentials, setIsLoadingCredentials] = useState(false);
|
|
240
|
+
|
|
241
|
+
// Check if selected provider has active credentials
|
|
242
|
+
const hasActiveCredentials = useMemo(() => {
|
|
243
|
+
// Non-cloud mode or custom template: no credentials check needed
|
|
244
|
+
if (!isCloudMode || !selectedTemplate.providerId) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
// Check if provider is connected (handle both 'openai' and 'codex' for OpenAI)
|
|
248
|
+
if (selectedTemplate.providerId === 'codex') {
|
|
249
|
+
return connectedProviders.has('codex') || connectedProviders.has('openai');
|
|
250
|
+
}
|
|
251
|
+
return connectedProviders.has(selectedTemplate.providerId);
|
|
252
|
+
}, [isCloudMode, selectedTemplate.providerId, connectedProviders]);
|
|
253
|
+
|
|
254
|
+
// Provider setup URL for CTA
|
|
255
|
+
// Note: /providers/setup/ only supports 'claude' and 'codex'
|
|
256
|
+
// Other providers should go to /providers page
|
|
257
|
+
const providerSetupUrl = useMemo(() => {
|
|
258
|
+
if (!selectedTemplate.providerId) return null;
|
|
259
|
+
const command = selectedTemplate.command;
|
|
260
|
+
const supportedSetupPages = ['claude', 'codex'];
|
|
261
|
+
|
|
262
|
+
if (supportedSetupPages.includes(command)) {
|
|
263
|
+
const base = `/providers/setup/${command}`;
|
|
264
|
+
return workspaceId ? `${base}?workspace=${workspaceId}` : base;
|
|
265
|
+
} else {
|
|
266
|
+
// For other providers, go to main providers page
|
|
267
|
+
return workspaceId ? `/providers?workspace=${workspaceId}` : '/providers';
|
|
268
|
+
}
|
|
269
|
+
}, [selectedTemplate, workspaceId]);
|
|
270
|
+
|
|
271
|
+
// Fetch connected providers when modal opens in cloud mode
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!isOpen || !isCloudMode || !workspaceId) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const fetchProviders = async () => {
|
|
278
|
+
setIsLoadingCredentials(true);
|
|
279
|
+
try {
|
|
280
|
+
// Get workspace-specific provider connection status
|
|
281
|
+
const result = await cloudApi.getProviders(workspaceId);
|
|
282
|
+
if (result.success && result.data.providers) {
|
|
283
|
+
const providers = new Set(
|
|
284
|
+
result.data.providers
|
|
285
|
+
.filter(p => p.isConnected)
|
|
286
|
+
.map(p => p.id)
|
|
287
|
+
);
|
|
288
|
+
setConnectedProviders(providers);
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('Failed to fetch provider credentials:', err);
|
|
292
|
+
} finally {
|
|
293
|
+
setIsLoadingCredentials(false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
fetchProviders();
|
|
298
|
+
}, [isOpen, isCloudMode, workspaceId]);
|
|
299
|
+
|
|
300
|
+
const SPEAK_ON_OPTIONS: { value: SpeakOnTrigger; label: string; description: string }[] = [
|
|
301
|
+
{ value: 'EXPLICIT_ASK', label: 'Explicit Ask', description: 'When directly asked' },
|
|
302
|
+
{ value: 'SESSION_END', label: 'Session End', description: 'When session ends' },
|
|
303
|
+
{ value: 'CODE_WRITTEN', label: 'Code Written', description: 'When code is written' },
|
|
304
|
+
{ value: 'REVIEW_REQUEST', label: 'Review Request', description: 'When review requested' },
|
|
305
|
+
{ value: 'ALL_MESSAGES', label: 'All Messages', description: 'On every message' },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const suggestedName = useCallback(() => {
|
|
309
|
+
const prefix = selectedTemplate.id === 'claude' ? 'claude' : selectedTemplate.id;
|
|
310
|
+
let num = 1;
|
|
311
|
+
while (existingAgents.includes(`${prefix}-${num}`)) {
|
|
312
|
+
num++;
|
|
313
|
+
}
|
|
314
|
+
return `${prefix}-${num}`;
|
|
315
|
+
}, [selectedTemplate, existingAgents]);
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (isOpen) {
|
|
319
|
+
// Determine default template based on settings
|
|
320
|
+
const defaultTemplateId = agentDefaults?.defaultCliType;
|
|
321
|
+
const defaultTemplate = defaultTemplateId
|
|
322
|
+
? AGENT_TEMPLATES.find(t => t.id === defaultTemplateId && !t.comingSoon) ?? AGENT_TEMPLATES[0]
|
|
323
|
+
: AGENT_TEMPLATES[0];
|
|
324
|
+
|
|
325
|
+
setSelectedTemplate(defaultTemplate);
|
|
326
|
+
setName('');
|
|
327
|
+
setCustomCommand('');
|
|
328
|
+
// Use settings-based model defaults with fallbacks
|
|
329
|
+
setSelectedModel(agentDefaults?.defaultModels?.claude ?? 'sonnet');
|
|
330
|
+
setSelectedCursorModel(agentDefaults?.defaultModels?.cursor ?? 'opus-4.5-thinking');
|
|
331
|
+
setSelectedCodexModel(agentDefaults?.defaultModels?.codex ?? 'gpt-5.2-codex');
|
|
332
|
+
setSelectedGeminiModel(agentDefaults?.defaultModels?.gemini ?? 'gemini-2.5-pro');
|
|
333
|
+
setCwd('');
|
|
334
|
+
setSelectedRepoId(activeRepoId);
|
|
335
|
+
setTeam('');
|
|
336
|
+
setIsShadow(false);
|
|
337
|
+
setShadowOf('');
|
|
338
|
+
setShadowAgent('');
|
|
339
|
+
setShadowSpeakOn(['EXPLICIT_ASK']);
|
|
340
|
+
setLocalError(null);
|
|
341
|
+
setTimeout(() => nameInputRef.current?.focus(), 100);
|
|
342
|
+
}
|
|
343
|
+
}, [isOpen, agentDefaults, activeRepoId, repos]);
|
|
344
|
+
|
|
345
|
+
const validateName = useCallback(
|
|
346
|
+
(value: string): string | null => {
|
|
347
|
+
if (!value.trim()) {
|
|
348
|
+
return 'Name is required';
|
|
349
|
+
}
|
|
350
|
+
if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(value)) {
|
|
351
|
+
return 'Name must start with a letter and contain only letters, numbers, and hyphens';
|
|
352
|
+
}
|
|
353
|
+
if (existingAgents.includes(value)) {
|
|
354
|
+
return 'An agent with this name already exists';
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
},
|
|
358
|
+
[existingAgents]
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
|
|
364
|
+
const finalName = name.trim() || suggestedName();
|
|
365
|
+
const nameError = validateName(finalName);
|
|
366
|
+
if (nameError) {
|
|
367
|
+
setLocalError(nameError);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const command = effectiveCommand;
|
|
372
|
+
if (!command.trim()) {
|
|
373
|
+
setLocalError('Command is required');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isShadow && !shadowOf) {
|
|
378
|
+
setLocalError('Please select an agent to shadow');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setLocalError(null);
|
|
383
|
+
|
|
384
|
+
// Derive cwd: in cloud mode with repos, use selected repo name; otherwise use text input
|
|
385
|
+
let effectiveCwd: string | undefined;
|
|
386
|
+
if (isCloudMode && repos && repos.length > 0 && selectedRepoId) {
|
|
387
|
+
if (selectedRepoId === '__all__') {
|
|
388
|
+
// Coordinator mode: no cwd, agent starts at workspace root with access to all repos
|
|
389
|
+
effectiveCwd = undefined;
|
|
390
|
+
} else {
|
|
391
|
+
const selectedRepo = repos.find(r => r.id === selectedRepoId);
|
|
392
|
+
if (selectedRepo) {
|
|
393
|
+
effectiveCwd = selectedRepo.githubFullName.split('/').pop();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
effectiveCwd = cwd.trim() || undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const success = await onSpawn({
|
|
401
|
+
name: finalName,
|
|
402
|
+
command: command.trim(),
|
|
403
|
+
cwd: effectiveCwd,
|
|
404
|
+
team: team.trim() || undefined,
|
|
405
|
+
shadowMode: shadowMode,
|
|
406
|
+
shadowOf: isShadow ? shadowOf : undefined,
|
|
407
|
+
shadowAgent: shadowAgent.trim() || undefined,
|
|
408
|
+
shadowTriggers: isShadow ? shadowSpeakOn : undefined,
|
|
409
|
+
shadowSpeakOn: isShadow ? shadowSpeakOn : undefined,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (success) {
|
|
413
|
+
onClose();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
418
|
+
if (e.key === 'Escape') {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
onClose();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
if (!isOpen) return null;
|
|
425
|
+
|
|
426
|
+
const colors = name ? getAgentColor(name) : getAgentColor(suggestedName());
|
|
427
|
+
const displayError = error || localError;
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div
|
|
431
|
+
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000] animate-fade-in"
|
|
432
|
+
onClick={onClose}
|
|
433
|
+
>
|
|
434
|
+
<div
|
|
435
|
+
className="relative bg-bg-primary border border-border rounded-xl w-[480px] max-w-[90vw] max-h-[90vh] overflow-y-auto shadow-modal animate-slide-up"
|
|
436
|
+
onClick={(e) => e.stopPropagation()}
|
|
437
|
+
onKeyDown={handleKeyDown}
|
|
438
|
+
>
|
|
439
|
+
{isSpawning && (
|
|
440
|
+
<SpawningOverlay
|
|
441
|
+
agentName={name.trim() || suggestedName()}
|
|
442
|
+
colors={colors}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
<div className="flex items-center justify-between p-5 border-b border-border">
|
|
446
|
+
<h2 className="m-0 text-lg font-semibold text-text-primary">Spawn New Agent</h2>
|
|
447
|
+
<button
|
|
448
|
+
className="flex items-center justify-center w-8 h-8 bg-transparent border-none rounded-md text-text-muted cursor-pointer transition-all duration-150 hover:bg-bg-hover hover:text-text-primary"
|
|
449
|
+
onClick={onClose}
|
|
450
|
+
aria-label="Close"
|
|
451
|
+
>
|
|
452
|
+
<CloseIcon />
|
|
453
|
+
</button>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<form onSubmit={handleSubmit} className="p-6">
|
|
457
|
+
{/* Agent Type Selection */}
|
|
458
|
+
<div className="mb-5">
|
|
459
|
+
<label className="block text-sm font-semibold text-text-primary mb-2">Agent Type</label>
|
|
460
|
+
<div className="grid grid-cols-3 gap-2">
|
|
461
|
+
{AGENT_TEMPLATES.map((template) => {
|
|
462
|
+
// Only disable "coming soon" providers in cloud mode - locally they might be available
|
|
463
|
+
const isDisabled = template.comingSoon && isCloudMode;
|
|
464
|
+
return (
|
|
465
|
+
<button
|
|
466
|
+
key={template.id}
|
|
467
|
+
type="button"
|
|
468
|
+
disabled={isDisabled}
|
|
469
|
+
className={`
|
|
470
|
+
flex flex-col items-center gap-1 py-3 px-2 border-2 rounded-lg font-sans transition-all duration-150 relative
|
|
471
|
+
${isDisabled
|
|
472
|
+
? 'opacity-50 cursor-not-allowed bg-bg-hover border-transparent'
|
|
473
|
+
: selectedTemplate.id === template.id
|
|
474
|
+
? 'bg-accent/10 border-accent cursor-pointer'
|
|
475
|
+
: 'bg-bg-hover border-transparent hover:bg-bg-active cursor-pointer'
|
|
476
|
+
}
|
|
477
|
+
`}
|
|
478
|
+
onClick={() => !isDisabled && setSelectedTemplate(template)}
|
|
479
|
+
>
|
|
480
|
+
{isDisabled && (
|
|
481
|
+
<span className="absolute top-1 right-1 px-1.5 py-0.5 bg-amber-400/20 text-amber-400 text-[10px] font-medium rounded">
|
|
482
|
+
Soon
|
|
483
|
+
</span>
|
|
484
|
+
)}
|
|
485
|
+
<span className={`text-2xl ${isDisabled ? 'grayscale' : ''}`}>{template.icon}</span>
|
|
486
|
+
<span className="text-sm font-semibold text-text-primary">{template.name}</span>
|
|
487
|
+
<span className="text-xs text-text-muted text-center">{template.description}</span>
|
|
488
|
+
</button>
|
|
489
|
+
);
|
|
490
|
+
})}
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{/* Model Selection (Claude only) */}
|
|
495
|
+
{selectedTemplate.id === 'claude' && (
|
|
496
|
+
<div className="mb-5">
|
|
497
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="claude-model">
|
|
498
|
+
Model
|
|
499
|
+
</label>
|
|
500
|
+
<select
|
|
501
|
+
id="claude-model"
|
|
502
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
503
|
+
value={selectedModel}
|
|
504
|
+
onChange={(e) => setSelectedModel(e.target.value as ClaudeModel)}
|
|
505
|
+
disabled={isSpawning}
|
|
506
|
+
>
|
|
507
|
+
{CLAUDE_MODEL_OPTIONS.map((model) => (
|
|
508
|
+
<option key={model.value} value={model.value}>
|
|
509
|
+
{model.label}
|
|
510
|
+
</option>
|
|
511
|
+
))}
|
|
512
|
+
</select>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{/* Model Selection (Cursor only) */}
|
|
517
|
+
{selectedTemplate.id === 'cursor' && (
|
|
518
|
+
<div className="mb-5">
|
|
519
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="cursor-model">
|
|
520
|
+
Model
|
|
521
|
+
</label>
|
|
522
|
+
<select
|
|
523
|
+
id="cursor-model"
|
|
524
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
525
|
+
value={selectedCursorModel}
|
|
526
|
+
onChange={(e) => setSelectedCursorModel(e.target.value as CursorModel)}
|
|
527
|
+
disabled={isSpawning}
|
|
528
|
+
>
|
|
529
|
+
{CURSOR_MODEL_OPTIONS.map((model) => (
|
|
530
|
+
<option key={model.value} value={model.value}>
|
|
531
|
+
{model.label}
|
|
532
|
+
</option>
|
|
533
|
+
))}
|
|
534
|
+
</select>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
{/* Model Selection (Codex only) */}
|
|
539
|
+
{selectedTemplate.id === 'codex' && (
|
|
540
|
+
<div className="mb-5">
|
|
541
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="codex-model">
|
|
542
|
+
Model
|
|
543
|
+
</label>
|
|
544
|
+
<select
|
|
545
|
+
id="codex-model"
|
|
546
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
547
|
+
value={selectedCodexModel}
|
|
548
|
+
onChange={(e) => setSelectedCodexModel(e.target.value as CodexModel)}
|
|
549
|
+
disabled={isSpawning}
|
|
550
|
+
>
|
|
551
|
+
{CODEX_MODEL_OPTIONS.map((model) => (
|
|
552
|
+
<option key={model.value} value={model.value}>
|
|
553
|
+
{model.label}
|
|
554
|
+
</option>
|
|
555
|
+
))}
|
|
556
|
+
</select>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
|
|
560
|
+
{/* Model Selection (Gemini only) */}
|
|
561
|
+
{selectedTemplate.id === 'gemini' && (
|
|
562
|
+
<div className="mb-5">
|
|
563
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="gemini-model">
|
|
564
|
+
Model
|
|
565
|
+
</label>
|
|
566
|
+
<select
|
|
567
|
+
id="gemini-model"
|
|
568
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
569
|
+
value={selectedGeminiModel}
|
|
570
|
+
onChange={(e) => setSelectedGeminiModel(e.target.value as GeminiModel)}
|
|
571
|
+
disabled={isSpawning}
|
|
572
|
+
>
|
|
573
|
+
{GEMINI_MODEL_OPTIONS.map((model) => (
|
|
574
|
+
<option key={model.value} value={model.value}>
|
|
575
|
+
{model.label}
|
|
576
|
+
</option>
|
|
577
|
+
))}
|
|
578
|
+
</select>
|
|
579
|
+
</div>
|
|
580
|
+
)}
|
|
581
|
+
|
|
582
|
+
{/* Agent Name */}
|
|
583
|
+
<div className="mb-5">
|
|
584
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="agent-name">
|
|
585
|
+
Agent Name
|
|
586
|
+
</label>
|
|
587
|
+
<div className="flex items-center gap-3">
|
|
588
|
+
<div
|
|
589
|
+
className="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center text-sm font-semibold"
|
|
590
|
+
style={{ backgroundColor: colors.primary, color: colors.text }}
|
|
591
|
+
>
|
|
592
|
+
{getAgentInitials(name || suggestedName())}
|
|
593
|
+
</div>
|
|
594
|
+
<input
|
|
595
|
+
ref={nameInputRef}
|
|
596
|
+
id="agent-name"
|
|
597
|
+
type="text"
|
|
598
|
+
className="flex-1 py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-transparent text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted placeholder:text-text-muted"
|
|
599
|
+
placeholder={suggestedName()}
|
|
600
|
+
value={name}
|
|
601
|
+
onChange={(e) => {
|
|
602
|
+
setName(e.target.value);
|
|
603
|
+
setLocalError(null);
|
|
604
|
+
}}
|
|
605
|
+
disabled={isSpawning}
|
|
606
|
+
/>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
{/* Team Assignment - moved higher for prominence */}
|
|
611
|
+
<div className="mb-5">
|
|
612
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="agent-team">
|
|
613
|
+
Team <span className="font-normal text-text-muted">(optional)</span>
|
|
614
|
+
</label>
|
|
615
|
+
<input
|
|
616
|
+
id="agent-team"
|
|
617
|
+
type="text"
|
|
618
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-transparent text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted placeholder:text-text-muted"
|
|
619
|
+
placeholder="e.g., frontend, backend, infra"
|
|
620
|
+
value={team}
|
|
621
|
+
onChange={(e) => setTeam(e.target.value)}
|
|
622
|
+
disabled={isSpawning}
|
|
623
|
+
/>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* Custom Command (if custom template) */}
|
|
627
|
+
{selectedTemplate.id === 'custom' && (
|
|
628
|
+
<div className="mb-5">
|
|
629
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="agent-command">
|
|
630
|
+
Command
|
|
631
|
+
</label>
|
|
632
|
+
<input
|
|
633
|
+
id="agent-command"
|
|
634
|
+
type="text"
|
|
635
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-transparent text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted placeholder:text-text-muted"
|
|
636
|
+
placeholder="e.g., python agent.py"
|
|
637
|
+
value={customCommand}
|
|
638
|
+
onChange={(e) => setCustomCommand(e.target.value)}
|
|
639
|
+
disabled={isSpawning}
|
|
640
|
+
/>
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
|
|
644
|
+
{/* Repository (cloud) / Working Directory (local) */}
|
|
645
|
+
{isCloudMode && repos && repos.length > 0 ? (
|
|
646
|
+
<div className="mb-5">
|
|
647
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="agent-repo">
|
|
648
|
+
Repository
|
|
649
|
+
</label>
|
|
650
|
+
<select
|
|
651
|
+
id="agent-repo"
|
|
652
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-transparent text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
653
|
+
value={selectedRepoId || ''}
|
|
654
|
+
onChange={(e) => setSelectedRepoId(e.target.value)}
|
|
655
|
+
disabled={isSpawning}
|
|
656
|
+
>
|
|
657
|
+
{repos.length > 1 && (
|
|
658
|
+
<option value="__all__">All Repositories (Coordinator)</option>
|
|
659
|
+
)}
|
|
660
|
+
{repos.map((repo) => (
|
|
661
|
+
<option key={repo.id} value={repo.id}>
|
|
662
|
+
{repo.githubFullName}
|
|
663
|
+
</option>
|
|
664
|
+
))}
|
|
665
|
+
</select>
|
|
666
|
+
{selectedRepoId === '__all__' && (
|
|
667
|
+
<p className="mt-1.5 text-xs text-accent-purple">
|
|
668
|
+
Agent will have access to all repositories in this workspace
|
|
669
|
+
</p>
|
|
670
|
+
)}
|
|
671
|
+
</div>
|
|
672
|
+
) : (
|
|
673
|
+
<div className="mb-5">
|
|
674
|
+
<label className="block text-sm font-semibold text-text-primary mb-2" htmlFor="agent-cwd">
|
|
675
|
+
Working Directory <span className="font-normal text-text-muted">(optional)</span>
|
|
676
|
+
</label>
|
|
677
|
+
<input
|
|
678
|
+
id="agent-cwd"
|
|
679
|
+
type="text"
|
|
680
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-transparent text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted placeholder:text-text-muted"
|
|
681
|
+
placeholder="Current directory"
|
|
682
|
+
value={cwd}
|
|
683
|
+
onChange={(e) => setCwd(e.target.value)}
|
|
684
|
+
disabled={isSpawning}
|
|
685
|
+
/>
|
|
686
|
+
</div>
|
|
687
|
+
)}
|
|
688
|
+
|
|
689
|
+
{/* Shadow Agent Configuration */}
|
|
690
|
+
<div className="mb-5 p-4 border border-border rounded-lg bg-bg-hover/50">
|
|
691
|
+
<div className="flex items-center justify-between mb-3">
|
|
692
|
+
<div>
|
|
693
|
+
<label className="block text-sm font-semibold text-text-primary">
|
|
694
|
+
Shadow Mode
|
|
695
|
+
</label>
|
|
696
|
+
<span className="text-xs text-text-muted">
|
|
697
|
+
Shadow execution: {shadowMode === 'subagent' ? 'Subagent (in-process)' : 'Process (separate)'}
|
|
698
|
+
</span>
|
|
699
|
+
</div>
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
className={`
|
|
703
|
+
relative w-11 h-6 rounded-full transition-colors duration-200
|
|
704
|
+
${isShadow ? 'bg-accent' : 'bg-bg-active'}
|
|
705
|
+
`}
|
|
706
|
+
onClick={() => setIsShadow(!isShadow)}
|
|
707
|
+
disabled={isSpawning}
|
|
708
|
+
aria-pressed={isShadow}
|
|
709
|
+
>
|
|
710
|
+
<span
|
|
711
|
+
className={`
|
|
712
|
+
absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform duration-200 shadow-sm
|
|
713
|
+
${isShadow ? 'translate-x-5' : 'translate-x-0'}
|
|
714
|
+
`}
|
|
715
|
+
/>
|
|
716
|
+
</button>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{isShadow && (
|
|
720
|
+
<>
|
|
721
|
+
{/* Primary Agent Selection */}
|
|
722
|
+
<div className="mb-4">
|
|
723
|
+
<label className="block text-sm font-medium text-text-secondary mb-2" htmlFor="shadow-of">
|
|
724
|
+
Shadow Agent
|
|
725
|
+
</label>
|
|
726
|
+
<select
|
|
727
|
+
id="shadow-of"
|
|
728
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted"
|
|
729
|
+
value={shadowOf}
|
|
730
|
+
onChange={(e) => setShadowOf(e.target.value)}
|
|
731
|
+
disabled={isSpawning}
|
|
732
|
+
>
|
|
733
|
+
<option value="">Select an agent to shadow...</option>
|
|
734
|
+
{existingAgents.map((agent) => (
|
|
735
|
+
<option key={agent} value={agent}>
|
|
736
|
+
{agent}
|
|
737
|
+
</option>
|
|
738
|
+
))}
|
|
739
|
+
</select>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
{/* Shadow Agent Profile (optional) */}
|
|
743
|
+
<div className="mb-4">
|
|
744
|
+
<label className="block text-sm font-medium text-text-secondary mb-2" htmlFor="shadow-agent">
|
|
745
|
+
Shadow Agent Profile <span className="font-normal text-text-muted">(optional)</span>
|
|
746
|
+
</label>
|
|
747
|
+
<input
|
|
748
|
+
id="shadow-agent"
|
|
749
|
+
type="text"
|
|
750
|
+
className="w-full py-2.5 px-3.5 border border-border rounded-md text-sm font-sans outline-none bg-bg-primary text-text-primary transition-colors duration-150 focus:border-accent disabled:bg-bg-hover disabled:text-text-muted placeholder:text-text-muted"
|
|
751
|
+
placeholder="e.g., shadow-reviewer"
|
|
752
|
+
value={shadowAgent}
|
|
753
|
+
onChange={(e) => setShadowAgent(e.target.value)}
|
|
754
|
+
disabled={isSpawning}
|
|
755
|
+
/>
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
{/* Speak On Triggers */}
|
|
759
|
+
<div>
|
|
760
|
+
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
761
|
+
Speak When
|
|
762
|
+
</label>
|
|
763
|
+
<div className="flex flex-wrap gap-2">
|
|
764
|
+
{SPEAK_ON_OPTIONS.map((option) => (
|
|
765
|
+
<button
|
|
766
|
+
key={option.value}
|
|
767
|
+
type="button"
|
|
768
|
+
className={`
|
|
769
|
+
py-1.5 px-3 rounded-md text-xs font-medium transition-all duration-150 border
|
|
770
|
+
${shadowSpeakOn.includes(option.value)
|
|
771
|
+
? 'bg-accent/20 border-accent text-accent'
|
|
772
|
+
: 'bg-bg-primary border-border text-text-secondary hover:bg-bg-active hover:text-text-primary'
|
|
773
|
+
}
|
|
774
|
+
`}
|
|
775
|
+
onClick={() => {
|
|
776
|
+
if (shadowSpeakOn.includes(option.value)) {
|
|
777
|
+
setShadowSpeakOn(shadowSpeakOn.filter(t => t !== option.value));
|
|
778
|
+
} else {
|
|
779
|
+
setShadowSpeakOn([...shadowSpeakOn, option.value]);
|
|
780
|
+
}
|
|
781
|
+
}}
|
|
782
|
+
disabled={isSpawning}
|
|
783
|
+
title={option.description}
|
|
784
|
+
>
|
|
785
|
+
{option.label}
|
|
786
|
+
</button>
|
|
787
|
+
))}
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
</>
|
|
791
|
+
)}
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
{/* Credentials CTA - shown when provider is not connected */}
|
|
795
|
+
{isCloudMode && !hasActiveCredentials && !isLoadingCredentials && selectedTemplate.providerId && (
|
|
796
|
+
<div className="p-4 bg-amber-400/10 border border-amber-400/30 rounded-lg mb-5">
|
|
797
|
+
<div className="flex items-start gap-3">
|
|
798
|
+
<div className="shrink-0 w-8 h-8 rounded-lg bg-amber-400/20 flex items-center justify-center">
|
|
799
|
+
<LockIcon />
|
|
800
|
+
</div>
|
|
801
|
+
<div className="flex-1">
|
|
802
|
+
<h4 className="text-sm font-semibold text-amber-400 mb-1">
|
|
803
|
+
{selectedTemplate.name} credentials required
|
|
804
|
+
</h4>
|
|
805
|
+
<p className="text-xs text-text-secondary mb-3">
|
|
806
|
+
Connect your {selectedTemplate.name} account to spawn {selectedTemplate.name} agents.
|
|
807
|
+
This enables secure access to the AI provider's API.
|
|
808
|
+
</p>
|
|
809
|
+
<a
|
|
810
|
+
href={providerSetupUrl || '#'}
|
|
811
|
+
className="inline-flex items-center gap-2 py-2 px-4 bg-amber-400 text-bg-deep font-semibold rounded-md text-sm hover:bg-amber-500 transition-colors"
|
|
812
|
+
>
|
|
813
|
+
<LockIcon />
|
|
814
|
+
Connect {selectedTemplate.name}
|
|
815
|
+
</a>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
)}
|
|
820
|
+
|
|
821
|
+
{/* Loading credentials indicator */}
|
|
822
|
+
{isCloudMode && isLoadingCredentials && (
|
|
823
|
+
<div className="flex items-center gap-2 p-3 bg-bg-hover rounded-md text-text-muted text-sm mb-5">
|
|
824
|
+
<Spinner />
|
|
825
|
+
<span>Checking provider credentials...</span>
|
|
826
|
+
</div>
|
|
827
|
+
)}
|
|
828
|
+
|
|
829
|
+
{/* Error Display */}
|
|
830
|
+
{displayError && (
|
|
831
|
+
<div className="flex items-center gap-2 p-3 bg-error/10 border border-error/30 rounded-md text-error text-sm mb-5">
|
|
832
|
+
<ErrorIcon />
|
|
833
|
+
<span>{displayError}</span>
|
|
834
|
+
</div>
|
|
835
|
+
)}
|
|
836
|
+
|
|
837
|
+
{/* Actions */}
|
|
838
|
+
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
className="flex items-center gap-1.5 py-2.5 px-4 border-none rounded-md text-sm font-medium cursor-pointer font-sans transition-all duration-150 bg-bg-hover text-text-secondary hover:bg-bg-active hover:text-text-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
|
842
|
+
onClick={onClose}
|
|
843
|
+
disabled={isSpawning}
|
|
844
|
+
>
|
|
845
|
+
Cancel
|
|
846
|
+
</button>
|
|
847
|
+
<button
|
|
848
|
+
type="submit"
|
|
849
|
+
className="flex items-center gap-1.5 py-2.5 px-4 border-none rounded-md text-sm font-medium cursor-pointer font-sans transition-all duration-150 bg-accent text-white hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
|
850
|
+
disabled={isSpawning || (isCloudMode && !hasActiveCredentials)}
|
|
851
|
+
title={!hasActiveCredentials && isCloudMode ? `Connect ${selectedTemplate.name} credentials first` : undefined}
|
|
852
|
+
>
|
|
853
|
+
<RocketIcon />
|
|
854
|
+
Spawn Agent
|
|
855
|
+
</button>
|
|
856
|
+
</div>
|
|
857
|
+
</form>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function CloseIcon() {
|
|
864
|
+
return (
|
|
865
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
866
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
867
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
868
|
+
</svg>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function ErrorIcon() {
|
|
873
|
+
return (
|
|
874
|
+
<svg className="shrink-0" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
875
|
+
<circle cx="12" cy="12" r="10" />
|
|
876
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
877
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
878
|
+
</svg>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function RocketIcon() {
|
|
883
|
+
return (
|
|
884
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
885
|
+
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
|
|
886
|
+
<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
|
|
887
|
+
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
|
|
888
|
+
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
|
|
889
|
+
</svg>
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function Spinner() {
|
|
894
|
+
return (
|
|
895
|
+
<svg className="animate-spin" width="16" height="16" viewBox="0 0 24 24">
|
|
896
|
+
<circle
|
|
897
|
+
cx="12"
|
|
898
|
+
cy="12"
|
|
899
|
+
r="10"
|
|
900
|
+
stroke="currentColor"
|
|
901
|
+
strokeWidth="2"
|
|
902
|
+
fill="none"
|
|
903
|
+
strokeDasharray="32"
|
|
904
|
+
strokeLinecap="round"
|
|
905
|
+
/>
|
|
906
|
+
</svg>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const SPAWNING_MESSAGES = [
|
|
911
|
+
'Initializing agent environment...',
|
|
912
|
+
'Loading model configuration...',
|
|
913
|
+
'Establishing communication channel...',
|
|
914
|
+
'Preparing workspace...',
|
|
915
|
+
'Almost ready...',
|
|
916
|
+
];
|
|
917
|
+
|
|
918
|
+
function SpawningOverlay({ agentName, colors }: { agentName: string; colors: { primary: string; text: string } }) {
|
|
919
|
+
const [messageIndex, setMessageIndex] = useState(0);
|
|
920
|
+
const [dots, setDots] = useState('');
|
|
921
|
+
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
const msgInterval = setInterval(() => {
|
|
924
|
+
setMessageIndex((prev) => (prev + 1) % SPAWNING_MESSAGES.length);
|
|
925
|
+
}, 2400);
|
|
926
|
+
return () => clearInterval(msgInterval);
|
|
927
|
+
}, []);
|
|
928
|
+
|
|
929
|
+
useEffect(() => {
|
|
930
|
+
const dotInterval = setInterval(() => {
|
|
931
|
+
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
|
|
932
|
+
}, 500);
|
|
933
|
+
return () => clearInterval(dotInterval);
|
|
934
|
+
}, []);
|
|
935
|
+
|
|
936
|
+
const initials = getAgentInitials(agentName);
|
|
937
|
+
|
|
938
|
+
return (
|
|
939
|
+
<div className="absolute inset-0 bg-bg-primary/95 backdrop-blur-sm flex flex-col items-center justify-center z-10 rounded-xl">
|
|
940
|
+
{/* Pulsing agent avatar */}
|
|
941
|
+
<div className="relative mb-6">
|
|
942
|
+
<div
|
|
943
|
+
className="absolute inset-0 rounded-full animate-ping opacity-20"
|
|
944
|
+
style={{ backgroundColor: colors.primary }}
|
|
945
|
+
/>
|
|
946
|
+
<div
|
|
947
|
+
className="relative w-16 h-16 rounded-full flex items-center justify-center text-xl font-bold animate-pulse"
|
|
948
|
+
style={{ backgroundColor: colors.primary, color: colors.text }}
|
|
949
|
+
>
|
|
950
|
+
{initials}
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
|
|
954
|
+
{/* Spawning label */}
|
|
955
|
+
<div className="text-lg font-semibold text-text-primary mb-2">
|
|
956
|
+
Spawning {agentName}{dots}
|
|
957
|
+
</div>
|
|
958
|
+
|
|
959
|
+
{/* Cycling status message */}
|
|
960
|
+
<div
|
|
961
|
+
className="text-sm text-text-muted transition-opacity duration-300 mb-6"
|
|
962
|
+
key={messageIndex}
|
|
963
|
+
style={{ animation: 'fadeInUp 0.3s ease-out' }}
|
|
964
|
+
>
|
|
965
|
+
{SPAWNING_MESSAGES[messageIndex]}
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
{/* Progress bar */}
|
|
969
|
+
<div className="w-48 h-1 bg-bg-hover rounded-full overflow-hidden">
|
|
970
|
+
<div
|
|
971
|
+
className="h-full rounded-full"
|
|
972
|
+
style={{
|
|
973
|
+
backgroundColor: colors.primary,
|
|
974
|
+
animation: 'spawningProgress 2.4s ease-in-out infinite',
|
|
975
|
+
}}
|
|
976
|
+
/>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
<style>{`
|
|
980
|
+
@keyframes fadeInUp {
|
|
981
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
982
|
+
to { opacity: 1; transform: translateY(0); }
|
|
983
|
+
}
|
|
984
|
+
@keyframes spawningProgress {
|
|
985
|
+
0% { width: 0%; margin-left: 0%; }
|
|
986
|
+
50% { width: 60%; margin-left: 20%; }
|
|
987
|
+
100% { width: 0%; margin-left: 100%; }
|
|
988
|
+
}
|
|
989
|
+
`}</style>
|
|
990
|
+
</div>
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function LockIcon() {
|
|
995
|
+
return (
|
|
996
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
997
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
998
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
999
|
+
</svg>
|
|
1000
|
+
);
|
|
1001
|
+
}
|