@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,202 @@
1
+ /**
2
+ * ServerCard Component
3
+ *
4
+ * Displays a fleet server's status, connected agents,
5
+ * and health metrics in a compact card format.
6
+ */
7
+
8
+ import React from 'react';
9
+
10
+ export interface ServerInfo {
11
+ id: string;
12
+ name: string;
13
+ url: string;
14
+ status: 'online' | 'offline' | 'degraded' | 'connecting';
15
+ agentCount: number;
16
+ messageRate?: number;
17
+ latency?: number;
18
+ uptime?: number;
19
+ version?: string;
20
+ region?: string;
21
+ lastSeen?: string | number;
22
+ }
23
+
24
+ export interface ServerCardProps {
25
+ server: ServerInfo;
26
+ isSelected?: boolean;
27
+ onClick?: () => void;
28
+ onReconnect?: () => void;
29
+ compact?: boolean;
30
+ }
31
+
32
+ export function ServerCard({
33
+ server,
34
+ isSelected = false,
35
+ onClick,
36
+ onReconnect,
37
+ compact = false,
38
+ }: ServerCardProps) {
39
+ const statusColor = getStatusColor(server.status);
40
+ const statusLabel = getStatusLabel(server.status);
41
+
42
+ if (compact) {
43
+ return (
44
+ <button
45
+ className={`
46
+ flex items-center gap-2 py-2 px-3 bg-bg-tertiary border border-border-subtle rounded-md cursor-pointer font-inherit transition-all duration-150
47
+ hover:bg-bg-hover
48
+ ${isSelected ? 'bg-bg-elevated border-accent-cyan' : ''}
49
+ ${server.status === 'offline' ? 'opacity-70' : ''}
50
+ `}
51
+ onClick={onClick}
52
+ >
53
+ <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: statusColor }} />
54
+ <span className="flex-1 text-sm font-medium text-text-primary text-left">{server.name}</span>
55
+ <span className="text-xs text-text-muted bg-bg-tertiary px-1.5 py-0.5 rounded-full">{server.agentCount}</span>
56
+ </button>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <div
62
+ className={`
63
+ bg-bg-card border border-border-subtle rounded-lg p-4 cursor-pointer transition-all duration-150
64
+ hover:border-border-hover hover:shadow-md
65
+ ${isSelected ? 'border-accent-cyan bg-bg-elevated' : ''}
66
+ ${server.status === 'offline' ? 'opacity-70' : ''}
67
+ ${server.status === 'degraded' ? 'border-l-[3px] border-l-warning' : ''}
68
+ `}
69
+ onClick={onClick}
70
+ role={onClick ? 'button' : undefined}
71
+ tabIndex={onClick ? 0 : undefined}
72
+ >
73
+ {/* Header */}
74
+ <div className="flex items-start justify-between mb-4">
75
+ <div className="flex items-center gap-3">
76
+ <ServerIcon />
77
+ <div className="flex flex-col">
78
+ <span className="font-semibold text-sm text-text-primary">{server.name}</span>
79
+ {server.region && (
80
+ <span className="text-xs text-text-muted">{server.region}</span>
81
+ )}
82
+ </div>
83
+ </div>
84
+ <div className="flex items-center gap-1.5 text-xs font-medium" style={{ color: statusColor }}>
85
+ <span
86
+ className={`w-2 h-2 rounded-full flex-shrink-0 ${server.status === 'connecting' ? 'animate-pulse' : ''}`}
87
+ style={{ backgroundColor: statusColor }}
88
+ />
89
+ <span>{statusLabel}</span>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Metrics */}
94
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(60px,1fr))] gap-3 mb-4">
95
+ <div className="flex flex-col items-center text-center">
96
+ <span className="text-lg font-semibold text-text-primary">{server.agentCount}</span>
97
+ <span className="text-[11px] text-text-muted uppercase tracking-wide">Agents</span>
98
+ </div>
99
+ {server.messageRate !== undefined && (
100
+ <div className="flex flex-col items-center text-center">
101
+ <span className="text-lg font-semibold text-text-primary">{server.messageRate}/s</span>
102
+ <span className="text-[11px] text-text-muted uppercase tracking-wide">Messages</span>
103
+ </div>
104
+ )}
105
+ {server.latency !== undefined && (
106
+ <div className="flex flex-col items-center text-center">
107
+ <span className="text-lg font-semibold text-text-primary">{server.latency}ms</span>
108
+ <span className="text-[11px] text-text-muted uppercase tracking-wide">Latency</span>
109
+ </div>
110
+ )}
111
+ {server.uptime !== undefined && (
112
+ <div className="flex flex-col items-center text-center">
113
+ <span className="text-lg font-semibold text-text-primary">{formatUptime(server.uptime)}</span>
114
+ <span className="text-[11px] text-text-muted uppercase tracking-wide">Uptime</span>
115
+ </div>
116
+ )}
117
+ </div>
118
+
119
+ {/* Footer */}
120
+ <div className="flex items-center justify-between pt-3 border-t border-border-subtle">
121
+ <span className="text-[11px] text-text-muted font-mono">{server.url}</span>
122
+ {server.version && (
123
+ <span className="text-[11px] text-text-muted bg-bg-tertiary px-1.5 py-0.5 rounded">v{server.version}</span>
124
+ )}
125
+ </div>
126
+
127
+ {/* Reconnect button */}
128
+ {server.status === 'offline' && onReconnect && (
129
+ <button
130
+ className="flex items-center justify-center gap-1.5 w-full mt-3 py-2 px-3 bg-error/10 border border-error/30 rounded-md text-error text-xs font-medium cursor-pointer font-inherit transition-all duration-150 hover:bg-error/20 hover:border-error/50"
131
+ onClick={(e) => {
132
+ e.stopPropagation();
133
+ onReconnect();
134
+ }}
135
+ >
136
+ <RefreshIcon />
137
+ Reconnect
138
+ </button>
139
+ )}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ // Helper functions
145
+ function getStatusColor(status: ServerInfo['status']): string {
146
+ switch (status) {
147
+ case 'online':
148
+ return '#10b981';
149
+ case 'offline':
150
+ return '#ef4444';
151
+ case 'degraded':
152
+ return '#f59e0b';
153
+ case 'connecting':
154
+ return '#6366f1';
155
+ default:
156
+ return '#888888';
157
+ }
158
+ }
159
+
160
+ function getStatusLabel(status: ServerInfo['status']): string {
161
+ switch (status) {
162
+ case 'online':
163
+ return 'Online';
164
+ case 'offline':
165
+ return 'Offline';
166
+ case 'degraded':
167
+ return 'Degraded';
168
+ case 'connecting':
169
+ return 'Connecting...';
170
+ default:
171
+ return 'Unknown';
172
+ }
173
+ }
174
+
175
+ function formatUptime(seconds: number): string {
176
+ if (seconds < 60) return `${seconds}s`;
177
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
178
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
179
+ return `${Math.floor(seconds / 86400)}d`;
180
+ }
181
+
182
+ // Icon components
183
+ function ServerIcon() {
184
+ return (
185
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
186
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
187
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
188
+ <line x1="6" y1="6" x2="6.01" y2="6" />
189
+ <line x1="6" y1="18" x2="6.01" y2="18" />
190
+ </svg>
191
+ );
192
+ }
193
+
194
+ function RefreshIcon() {
195
+ return (
196
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
197
+ <polyline points="23 4 23 10 17 10" />
198
+ <polyline points="1 20 1 14 7 14" />
199
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
200
+ </svg>
201
+ );
202
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Session Expired Modal
3
+ *
4
+ * Displayed when the user's session has expired and they need to log in again.
5
+ * Provides a clear message and easy path to re-authenticate.
6
+ */
7
+
8
+ import React from 'react';
9
+ import type { SessionError } from './hooks/useSession';
10
+
11
+ export interface SessionExpiredModalProps {
12
+ /** Whether the modal is visible */
13
+ isOpen: boolean;
14
+ /** Session error details */
15
+ error: SessionError | null;
16
+ /** Called when user clicks to log in */
17
+ onLogin: () => void;
18
+ /** Called when modal is dismissed (optional) */
19
+ onDismiss?: () => void;
20
+ }
21
+
22
+ export function SessionExpiredModal({
23
+ isOpen,
24
+ error,
25
+ onLogin,
26
+ onDismiss,
27
+ }: SessionExpiredModalProps) {
28
+ if (!isOpen) return null;
29
+
30
+ const getMessage = () => {
31
+ if (!error) return 'Your session has expired. Please log in again to continue.';
32
+
33
+ switch (error.code) {
34
+ case 'SESSION_EXPIRED':
35
+ return 'Your session has expired. Please log in again to continue.';
36
+ case 'USER_NOT_FOUND':
37
+ return 'Your account was not found. Please log in again.';
38
+ case 'SESSION_ERROR':
39
+ return 'There was a problem with your session. Please log in again.';
40
+ default:
41
+ return error.message || 'Your session has expired. Please log in again.';
42
+ }
43
+ };
44
+
45
+ return (
46
+ <>
47
+ {/* Backdrop */}
48
+ <div
49
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[9998]"
50
+ onClick={onDismiss}
51
+ aria-hidden="true"
52
+ />
53
+
54
+ {/* Modal */}
55
+ <div
56
+ className="fixed inset-0 flex items-center justify-center z-[9999] p-4"
57
+ role="dialog"
58
+ aria-modal="true"
59
+ aria-labelledby="session-expired-title"
60
+ >
61
+ <div className="bg-bg-primary rounded-lg shadow-xl max-w-md w-full p-6 animate-in fade-in zoom-in-95 duration-200">
62
+ {/* Icon */}
63
+ <div className="flex justify-center mb-4">
64
+ <div className="w-16 h-16 rounded-full bg-warning/10 flex items-center justify-center">
65
+ <svg
66
+ className="w-8 h-8 text-warning"
67
+ fill="none"
68
+ viewBox="0 0 24 24"
69
+ stroke="currentColor"
70
+ strokeWidth={2}
71
+ >
72
+ <path
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
76
+ />
77
+ </svg>
78
+ </div>
79
+ </div>
80
+
81
+ {/* Title */}
82
+ <h2
83
+ id="session-expired-title"
84
+ className="text-xl font-semibold text-text-primary text-center mb-2"
85
+ >
86
+ Session Expired
87
+ </h2>
88
+
89
+ {/* Message */}
90
+ <p className="text-text-muted text-center mb-6">
91
+ {getMessage()}
92
+ </p>
93
+
94
+ {/* Actions */}
95
+ <div className="flex flex-col gap-3">
96
+ <button
97
+ onClick={onLogin}
98
+ className="w-full py-3 px-4 bg-accent text-white font-medium rounded-lg
99
+ hover:bg-accent-hover transition-colors duration-200
100
+ focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2
101
+ focus:ring-offset-bg-primary"
102
+ >
103
+ Log In Again
104
+ </button>
105
+
106
+ {onDismiss && (
107
+ <button
108
+ onClick={onDismiss}
109
+ className="w-full py-3 px-4 text-text-muted hover:text-text-primary
110
+ font-medium rounded-lg transition-colors duration-200
111
+ hover:bg-bg-secondary"
112
+ >
113
+ Dismiss
114
+ </button>
115
+ )}
116
+ </div>
117
+
118
+ {/* Help text */}
119
+ <p className="text-xs text-text-muted text-center mt-4">
120
+ You'll be redirected to the login page where you can sign in with GitHub.
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </>
125
+ );
126
+ }
127
+
128
+ export default SessionExpiredModal;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Tests for SpawnModal component
3
+ *
4
+ * Covers: repo dropdown in cloud mode, working directory in local mode,
5
+ * cwd derivation from selected repo, and activeRepoId pre-selection.
6
+ */
7
+
8
+ // @vitest-environment jsdom
9
+ import React from 'react';
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
12
+ import { SpawnModal } from './SpawnModal';
13
+
14
+ const MOCK_WORKSPACE_ID = '12345678-1234-1234-1234-123456789012';
15
+
16
+ // Mock cloudApi to return connected providers so the button is enabled
17
+ vi.mock('../lib/cloudApi', () => ({
18
+ cloudApi: {
19
+ getProviders: vi.fn().mockResolvedValue({
20
+ success: true,
21
+ data: {
22
+ providers: [
23
+ { id: 'anthropic', name: 'Claude', displayName: 'Claude', isConnected: true },
24
+ { id: 'codex', name: 'Codex', displayName: 'Codex', isConnected: true },
25
+ ],
26
+ },
27
+ }),
28
+ },
29
+ }));
30
+
31
+ const mockRepos = [
32
+ { id: 'repo-1', githubFullName: 'AgentWorkforce/relay' },
33
+ { id: 'repo-2', githubFullName: 'AgentWorkforce/trajectories' },
34
+ { id: 'repo-3', githubFullName: 'AgentWorkforce/relay-cloud' },
35
+ ];
36
+
37
+ function getForm(): HTMLFormElement {
38
+ const form = document.querySelector('form');
39
+ if (!form) throw new Error('Form not found');
40
+ return form;
41
+ }
42
+
43
+ function renderSpawnModal(overrides: Partial<React.ComponentProps<typeof SpawnModal>> = {}) {
44
+ const defaultProps: React.ComponentProps<typeof SpawnModal> = {
45
+ isOpen: true,
46
+ onClose: vi.fn(),
47
+ onSpawn: vi.fn().mockResolvedValue(true),
48
+ existingAgents: [],
49
+ ...overrides,
50
+ };
51
+ return { ...render(<SpawnModal {...defaultProps} />), props: defaultProps };
52
+ }
53
+
54
+ describe('SpawnModal', () => {
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ });
58
+
59
+ afterEach(() => {
60
+ cleanup();
61
+ });
62
+
63
+ describe('repo dropdown (cloud mode)', () => {
64
+ it('shows repo dropdown when isCloudMode and repos are provided', () => {
65
+ renderSpawnModal({ isCloudMode: true, repos: mockRepos });
66
+ expect(screen.getByLabelText('Repository')).toBeTruthy();
67
+ expect(screen.queryByLabelText(/Working Directory/)).toBeNull();
68
+ });
69
+
70
+ it('lists all repos in the dropdown', () => {
71
+ renderSpawnModal({ isCloudMode: true, repos: mockRepos });
72
+ const select = screen.getByLabelText('Repository') as HTMLSelectElement;
73
+ const options = select.querySelectorAll('option');
74
+ // 3 repos + "All Repositories (Coordinator)" option when repos.length > 1
75
+ expect(options.length).toBe(4);
76
+ expect(options[0].textContent).toBe('All Repositories (Coordinator)');
77
+ expect(options[1].textContent).toBe('AgentWorkforce/relay');
78
+ expect(options[2].textContent).toBe('AgentWorkforce/trajectories');
79
+ expect(options[3].textContent).toBe('AgentWorkforce/relay-cloud');
80
+ });
81
+
82
+ it('pre-selects the active repo', async () => {
83
+ renderSpawnModal({
84
+ isCloudMode: true,
85
+ repos: mockRepos,
86
+ activeRepoId: 'repo-2',
87
+ });
88
+ // Wait for useEffect to run and set selectedRepoId
89
+ await waitFor(() => {
90
+ const select = screen.getByLabelText('Repository') as HTMLSelectElement;
91
+ expect(select.value).toBe('repo-2');
92
+ });
93
+ });
94
+
95
+ it('defaults to All Repositories when no activeRepoId and multiple repos', () => {
96
+ renderSpawnModal({ isCloudMode: true, repos: mockRepos });
97
+ const select = screen.getByLabelText('Repository') as HTMLSelectElement;
98
+ expect(select.value).toBe('__all__');
99
+ });
100
+
101
+ it('derives cwd from selected repo githubFullName on submit', async () => {
102
+ const onSpawn = vi.fn().mockResolvedValue(true);
103
+ renderSpawnModal({
104
+ isCloudMode: true,
105
+ repos: mockRepos,
106
+ activeRepoId: 'repo-2',
107
+ workspaceId: MOCK_WORKSPACE_ID,
108
+ onSpawn,
109
+ });
110
+
111
+ // Wait for credential check to resolve and button to be enabled
112
+ await waitFor(() => {
113
+ const buttons = screen.getAllByRole('button');
114
+ const spawnBtn = buttons.find((b) => b.textContent?.includes('Spawn Agent'));
115
+ expect(spawnBtn).toBeTruthy();
116
+ expect((spawnBtn as HTMLButtonElement).disabled).toBe(false);
117
+ });
118
+
119
+ fireEvent.submit(getForm());
120
+
121
+ await waitFor(() => {
122
+ expect(onSpawn).toHaveBeenCalled();
123
+ });
124
+
125
+ const config = onSpawn.mock.calls[0][0];
126
+ expect(config.cwd).toBe('trajectories');
127
+ });
128
+
129
+ it('allows changing the selected repo', async () => {
130
+ const onSpawn = vi.fn().mockResolvedValue(true);
131
+ renderSpawnModal({
132
+ isCloudMode: true,
133
+ repos: mockRepos,
134
+ activeRepoId: 'repo-1',
135
+ workspaceId: MOCK_WORKSPACE_ID,
136
+ onSpawn,
137
+ });
138
+
139
+ await waitFor(() => {
140
+ const buttons = screen.getAllByRole('button');
141
+ const spawnBtn = buttons.find((b) => b.textContent?.includes('Spawn Agent'));
142
+ expect((spawnBtn as HTMLButtonElement).disabled).toBe(false);
143
+ });
144
+
145
+ const select = screen.getByLabelText('Repository') as HTMLSelectElement;
146
+ fireEvent.change(select, { target: { value: 'repo-3' } });
147
+ expect(select.value).toBe('repo-3');
148
+
149
+ fireEvent.submit(getForm());
150
+
151
+ await waitFor(() => {
152
+ expect(onSpawn).toHaveBeenCalled();
153
+ });
154
+
155
+ const config = onSpawn.mock.calls[0][0];
156
+ expect(config.cwd).toBe('relay-cloud');
157
+ });
158
+ });
159
+
160
+ describe('working directory (local mode)', () => {
161
+ it('shows working directory input when not in cloud mode', () => {
162
+ renderSpawnModal({ isCloudMode: false });
163
+ expect(screen.getByLabelText(/Working Directory/)).toBeTruthy();
164
+ expect(screen.queryByLabelText('Repository')).toBeNull();
165
+ });
166
+
167
+ it('shows working directory input when cloud mode but no repos', () => {
168
+ renderSpawnModal({ isCloudMode: true, repos: [] });
169
+ expect(screen.getByLabelText(/Working Directory/)).toBeTruthy();
170
+ expect(screen.queryByLabelText('Repository')).toBeNull();
171
+ });
172
+
173
+ it('passes cwd from text input on submit', async () => {
174
+ const onSpawn = vi.fn().mockResolvedValue(true);
175
+ renderSpawnModal({ isCloudMode: false, onSpawn });
176
+
177
+ const cwdInput = screen.getByLabelText(/Working Directory/) as HTMLInputElement;
178
+ fireEvent.change(cwdInput, { target: { value: '/custom/path' } });
179
+
180
+ fireEvent.submit(getForm());
181
+
182
+ await waitFor(() => {
183
+ expect(onSpawn).toHaveBeenCalled();
184
+ });
185
+
186
+ const config = onSpawn.mock.calls[0][0];
187
+ expect(config.cwd).toBe('/custom/path');
188
+ });
189
+ });
190
+ });