@agent-relay/dashboard 2.0.80 → 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.
Files changed (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /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
+ }