@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.
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/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → 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,517 @@
1
+ /**
2
+ * TerminalProviderSetup Component
3
+ *
4
+ * Reusable component for terminal-based provider authentication setup.
5
+ * Handles agent spawning, interactive terminal, and cleanup.
6
+ * Users copy and paste auth URLs from the terminal output.
7
+ *
8
+ * Used in:
9
+ * - /providers/setup/[provider] page (full-page setup)
10
+ * - WorkspaceSettingsPanel (embedded setup)
11
+ */
12
+
13
+ import React, { useRef, useEffect, useCallback, useState } from 'react';
14
+ import { Terminal } from '@xterm/xterm';
15
+ import { FitAddon } from '@xterm/addon-fit';
16
+
17
+ export interface ProviderConfig {
18
+ id: string;
19
+ name: string;
20
+ displayName: string;
21
+ color: string;
22
+ }
23
+
24
+ export interface TerminalProviderSetupProps {
25
+ /** Provider configuration */
26
+ provider: ProviderConfig;
27
+ /** Workspace ID to spawn agent in */
28
+ workspaceId: string;
29
+ /** CSRF token for API requests */
30
+ csrfToken?: string;
31
+ /** Maximum height of the terminal */
32
+ maxHeight?: string;
33
+ /** Called when authentication is detected as complete */
34
+ onSuccess?: () => void;
35
+ /** Called when an error occurs */
36
+ onError?: (error: string) => void;
37
+ /** Called when cancel is requested */
38
+ onCancel?: () => void;
39
+ /** Called when user wants to connect another provider */
40
+ onConnectAnother?: () => void;
41
+ /** Whether to show header with close button */
42
+ showHeader?: boolean;
43
+ /** Custom class name */
44
+ className?: string;
45
+ }
46
+
47
+ // Terminal theme matching dashboard dark theme
48
+ const TERMINAL_THEME = {
49
+ background: '#0d0f14',
50
+ foreground: '#c9d1d9',
51
+ cursor: '#58a6ff',
52
+ cursorAccent: '#0d0f14',
53
+ selectionBackground: '#264f78',
54
+ selectionForeground: '#ffffff',
55
+ black: '#484f58',
56
+ red: '#f85149',
57
+ green: '#3fb950',
58
+ yellow: '#d29922',
59
+ blue: '#58a6ff',
60
+ magenta: '#bc8cff',
61
+ cyan: '#39c5cf',
62
+ white: '#b1bac4',
63
+ brightBlack: '#6e7681',
64
+ brightRed: '#ff7b72',
65
+ brightGreen: '#56d364',
66
+ brightYellow: '#e3b341',
67
+ brightBlue: '#79c0ff',
68
+ brightMagenta: '#d2a8ff',
69
+ brightCyan: '#56d4dd',
70
+ brightWhite: '#ffffff',
71
+ };
72
+
73
+
74
+ export function TerminalProviderSetup({
75
+ provider,
76
+ workspaceId,
77
+ csrfToken: initialCsrfToken,
78
+ maxHeight = '400px',
79
+ onSuccess,
80
+ onError,
81
+ onCancel,
82
+ onConnectAnother,
83
+ showHeader = true,
84
+ className = '',
85
+ }: TerminalProviderSetupProps) {
86
+ const containerRef = useRef<HTMLDivElement>(null);
87
+ const terminalRef = useRef<Terminal | null>(null);
88
+ const fitAddonRef = useRef<FitAddon | null>(null);
89
+ const wsRef = useRef<WebSocket | null>(null);
90
+ const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
91
+ const hasShownConnectedRef = useRef(false); // Prevent duplicate "Connected" messages
92
+ const onDataDisposableRef = useRef<{ dispose: () => void } | null>(null); // Track onData handler for cleanup
93
+
94
+ const [isSpawning, setIsSpawning] = useState(false);
95
+ const [isConnected, setIsConnected] = useState(false);
96
+ const [isConnecting, setIsConnecting] = useState(false);
97
+ const [error, setError] = useState<string | null>(null);
98
+ const [agentName, setAgentName] = useState<string | null>(null);
99
+ const [isComplete, setIsComplete] = useState(false);
100
+ const [csrfToken, setCsrfToken] = useState<string | undefined>(initialCsrfToken);
101
+
102
+ // Generate unique agent name
103
+ const generateAgentName = useCallback(() => {
104
+ const timestamp = Date.now().toString(36);
105
+ const random = Math.random().toString(36).substring(2, 6);
106
+ return `__setup__${provider.id}-${timestamp}${random}`;
107
+ }, [provider.id]);
108
+
109
+ // Fetch CSRF token if not provided
110
+ useEffect(() => {
111
+ if (!csrfToken) {
112
+ fetch('/api/auth/session', { credentials: 'include' })
113
+ .then(res => {
114
+ const token = res.headers.get('X-CSRF-Token');
115
+ if (token) setCsrfToken(token);
116
+ })
117
+ .catch(() => {});
118
+ }
119
+ }, [csrfToken]);
120
+
121
+ // Cleanup agent
122
+ const cleanupAgent = useCallback(async () => {
123
+ if (!workspaceId || !agentName) return;
124
+
125
+ try {
126
+ const headers: Record<string, string> = {};
127
+ if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
128
+
129
+ await fetch(`/api/workspaces/${workspaceId}/agents/${encodeURIComponent(agentName)}`, {
130
+ method: 'DELETE',
131
+ credentials: 'include',
132
+ headers,
133
+ });
134
+ } catch {
135
+ // Ignore cleanup errors
136
+ }
137
+ }, [workspaceId, agentName, csrfToken]);
138
+
139
+ // Spawn agent
140
+ const spawnAgent = useCallback(async () => {
141
+ if (!workspaceId || !csrfToken) return;
142
+
143
+ setIsSpawning(true);
144
+ setError(null);
145
+
146
+ const name = generateAgentName();
147
+
148
+ try {
149
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
150
+ if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
151
+
152
+ const res = await fetch(`/api/workspaces/${workspaceId}/agents`, {
153
+ method: 'POST',
154
+ credentials: 'include',
155
+ headers,
156
+ body: JSON.stringify({
157
+ name,
158
+ provider: provider.id === 'anthropic' ? 'claude' : provider.id,
159
+ interactive: true, // Disable auto-accept prompts
160
+ }),
161
+ });
162
+
163
+ if (!res.ok) {
164
+ const data = await res.json();
165
+ throw new Error(data.error || 'Failed to spawn agent');
166
+ }
167
+
168
+ setAgentName(name);
169
+ } catch (err) {
170
+ const message = err instanceof Error ? err.message : 'Failed to spawn agent';
171
+ setError(message);
172
+ onError?.(message);
173
+ } finally {
174
+ setIsSpawning(false);
175
+ }
176
+ }, [workspaceId, csrfToken, provider.id, generateAgentName, onError]);
177
+
178
+ // Initialize terminal
179
+ useEffect(() => {
180
+ if (!containerRef.current) return;
181
+
182
+ const terminal = new Terminal({
183
+ theme: TERMINAL_THEME,
184
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
185
+ fontSize: 12,
186
+ lineHeight: 1.4,
187
+ convertEol: true,
188
+ scrollback: 10000,
189
+ cursorBlink: true,
190
+ cursorStyle: 'block',
191
+ disableStdin: false,
192
+ allowProposedApi: true,
193
+ });
194
+
195
+ const fitAddon = new FitAddon();
196
+ terminal.loadAddon(fitAddon);
197
+ terminal.open(containerRef.current);
198
+ fitAddon.fit();
199
+
200
+ terminalRef.current = terminal;
201
+ fitAddonRef.current = fitAddon;
202
+
203
+ // Handle resize
204
+ const resizeObserver = new ResizeObserver(() => {
205
+ fitAddon.fit();
206
+ });
207
+ resizeObserver.observe(containerRef.current);
208
+
209
+ return () => {
210
+ resizeObserver.disconnect();
211
+ terminal.dispose();
212
+ terminalRef.current = null;
213
+ fitAddonRef.current = null;
214
+ };
215
+ }, []);
216
+
217
+ // Connect WebSocket when agent is spawned
218
+ useEffect(() => {
219
+ if (!agentName || !workspaceId) return;
220
+
221
+ // Reset the connected message flag when agent changes
222
+ hasShownConnectedRef.current = false;
223
+
224
+ const connectWebSocket = () => {
225
+ // Don't reconnect if we already have an open connection
226
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
227
+ return;
228
+ }
229
+
230
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
231
+ const wsUrl = `${protocol}//${window.location.host}/ws/logs/${encodeURIComponent(workspaceId)}/${encodeURIComponent(agentName)}`;
232
+
233
+ setIsConnecting(true);
234
+ const ws = new WebSocket(wsUrl);
235
+ wsRef.current = ws;
236
+
237
+ ws.onopen = () => {
238
+ setIsConnected(true);
239
+ setIsConnecting(false);
240
+ // Only show connected message once per session
241
+ if (!hasShownConnectedRef.current) {
242
+ hasShownConnectedRef.current = true;
243
+ terminalRef.current?.writeln('\x1b[90m[Connected - Interactive Mode]\x1b[0m');
244
+ terminalRef.current?.writeln('\x1b[90m[Type directly to respond to prompts]\x1b[0m\n');
245
+ }
246
+ };
247
+
248
+ ws.onclose = () => {
249
+ setIsConnected(false);
250
+ setIsConnecting(false);
251
+
252
+ // Reconnect after delay (only if not intentionally closed)
253
+ reconnectTimeoutRef.current = setTimeout(connectWebSocket, 2000);
254
+ };
255
+
256
+ ws.onmessage = (event) => {
257
+ try {
258
+ const data = JSON.parse(event.data);
259
+
260
+ if (data.type === 'history' && Array.isArray(data.lines)) {
261
+ data.lines.forEach((line: string) => {
262
+ terminalRef.current?.writeln(line);
263
+ });
264
+ } else if (data.type === 'log' || data.type === 'output') {
265
+ const content = data.content || data.data || data.message || '';
266
+ if (content) {
267
+ terminalRef.current?.write(content);
268
+ }
269
+ }
270
+ } catch {
271
+ if (typeof event.data === 'string') {
272
+ terminalRef.current?.write(event.data);
273
+ }
274
+ }
275
+ };
276
+
277
+ // Clean up previous onData handler before adding new one
278
+ if (onDataDisposableRef.current) {
279
+ onDataDisposableRef.current.dispose();
280
+ onDataDisposableRef.current = null;
281
+ }
282
+
283
+ // Handle user input - store disposable for cleanup
284
+ // Suppress input when user has text selected (e.g., copying OAuth URL)
285
+ if (terminalRef.current) {
286
+ onDataDisposableRef.current = terminalRef.current.onData((data: string) => {
287
+ // Don't send input if user has text selected (likely copying a URL)
288
+ const selection = terminalRef.current?.getSelection();
289
+ if (selection && selection.length > 0) {
290
+ return; // User is selecting/copying text, don't send input
291
+ }
292
+ if (ws.readyState === WebSocket.OPEN) {
293
+ ws.send(JSON.stringify({ type: 'input', agent: agentName, data }));
294
+ }
295
+ });
296
+ }
297
+ };
298
+
299
+ connectWebSocket();
300
+
301
+ return () => {
302
+ if (reconnectTimeoutRef.current) {
303
+ clearTimeout(reconnectTimeoutRef.current);
304
+ reconnectTimeoutRef.current = null;
305
+ }
306
+ if (onDataDisposableRef.current) {
307
+ onDataDisposableRef.current.dispose();
308
+ onDataDisposableRef.current = null;
309
+ }
310
+ wsRef.current?.close();
311
+ wsRef.current = null;
312
+ };
313
+ }, [agentName, workspaceId]);
314
+
315
+ // Auto-spawn on mount
316
+ useEffect(() => {
317
+ if (csrfToken && !agentName && !isSpawning) {
318
+ spawnAgent();
319
+ }
320
+ }, [csrfToken, agentName, isSpawning, spawnAgent]);
321
+
322
+ // Cleanup on unmount
323
+ useEffect(() => {
324
+ return () => {
325
+ cleanupAgent();
326
+ };
327
+ }, [cleanupAgent]);
328
+
329
+ const handleComplete = useCallback(async () => {
330
+ // Mark provider as connected in the database
331
+ // Use provider.name (canonical backend name e.g. 'codex', 'anthropic')
332
+ const providerName = provider.name || provider.id;
333
+ try {
334
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
335
+ if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
336
+
337
+ const response = await fetch(`/api/onboarding/mark-connected/${providerName}`, {
338
+ method: 'POST',
339
+ credentials: 'include',
340
+ headers,
341
+ body: JSON.stringify({ workspaceId }),
342
+ });
343
+
344
+ if (!response.ok) {
345
+ console.error('Failed to mark provider as connected:', await response.text());
346
+ }
347
+ } catch (err) {
348
+ console.error('Error marking provider as connected:', err);
349
+ }
350
+
351
+ await cleanupAgent();
352
+ setIsComplete(true);
353
+ }, [cleanupAgent, provider.id, provider.name, csrfToken, workspaceId]);
354
+
355
+ const handleDone = useCallback(() => {
356
+ onSuccess?.();
357
+ }, [onSuccess]);
358
+
359
+ const handleConnectAnother = useCallback(() => {
360
+ onConnectAnother?.();
361
+ }, [onConnectAnother]);
362
+
363
+ const handleCancel = useCallback(async () => {
364
+ await cleanupAgent();
365
+ onCancel?.();
366
+ }, [cleanupAgent, onCancel]);
367
+
368
+ return (
369
+ <div className={`flex flex-col rounded-xl overflow-hidden border border-border-subtle ${className}`}>
370
+ {/* Header */}
371
+ {showHeader && (
372
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-tertiary">
373
+ <div className="flex items-center gap-3">
374
+ <div
375
+ className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm"
376
+ style={{ backgroundColor: provider.color }}
377
+ >
378
+ {provider.displayName[0]}
379
+ </div>
380
+ <div>
381
+ <h4 className="text-sm font-semibold text-text-primary">
382
+ {provider.displayName} Setup
383
+ </h4>
384
+ <p className="text-xs text-text-muted">Interactive terminal</p>
385
+ </div>
386
+ </div>
387
+ <div className="flex items-center gap-2">
388
+ {isConnected && (
389
+ <span className="flex items-center gap-1 px-2 py-1 rounded-full bg-success/15 text-xs text-success">
390
+ <span className="w-1.5 h-1.5 rounded-full bg-success animate-pulse" />
391
+ Connected
392
+ </span>
393
+ )}
394
+ {onCancel && (
395
+ <button
396
+ onClick={handleCancel}
397
+ className="p-1.5 rounded-lg hover:bg-bg-hover text-text-muted hover:text-text-primary transition-colors"
398
+ >
399
+ <CloseIcon />
400
+ </button>
401
+ )}
402
+ </div>
403
+ </div>
404
+ )}
405
+
406
+ {/* Success State */}
407
+ {isComplete ? (
408
+ <div className="flex flex-col items-center justify-center py-12 px-6">
409
+ <div
410
+ className="w-16 h-16 rounded-full flex items-center justify-center mb-4"
411
+ style={{ backgroundColor: `${provider.color}20` }}
412
+ >
413
+ <CheckIcon className="w-8 h-8" style={{ color: provider.color }} />
414
+ </div>
415
+ <h3 className="text-lg font-semibold text-text-primary mb-2">
416
+ {provider.displayName} Connected!
417
+ </h3>
418
+ <p className="text-sm text-text-muted mb-6 text-center">
419
+ Your {provider.displayName} account has been successfully connected.
420
+ </p>
421
+ <div className="flex gap-3">
422
+ {onConnectAnother && (
423
+ <button
424
+ onClick={handleConnectAnother}
425
+ className="px-4 py-2 bg-bg-hover text-text-primary text-sm font-medium rounded-lg hover:bg-bg-tertiary transition-colors border border-border-subtle"
426
+ >
427
+ Connect Another Provider
428
+ </button>
429
+ )}
430
+ <button
431
+ onClick={handleDone}
432
+ className="px-4 py-2 bg-accent-cyan text-bg-deep text-sm font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors"
433
+ >
434
+ Continue to Dashboard
435
+ </button>
436
+ </div>
437
+ </div>
438
+ ) : (
439
+ <>
440
+ {/* Error */}
441
+ {error && (
442
+ <div className="px-4 py-3 bg-error/10 border-b border-error/30 text-sm text-error flex items-center gap-2">
443
+ <AlertIcon />
444
+ <span>{error}</span>
445
+ <button
446
+ onClick={spawnAgent}
447
+ className="ml-auto text-xs px-2 py-1 rounded bg-error/20 hover:bg-error/30 transition-colors"
448
+ >
449
+ Retry
450
+ </button>
451
+ </div>
452
+ )}
453
+
454
+ {/* Spawning indicator */}
455
+ {isSpawning && (
456
+ <div className="px-4 py-3 bg-accent-cyan/10 border-b border-accent-cyan/30 text-sm text-accent-cyan flex items-center gap-2">
457
+ <div className="w-4 h-4 border-2 border-accent-cyan/30 border-t-accent-cyan rounded-full animate-spin" />
458
+ <span>Starting {provider.displayName}...</span>
459
+ </div>
460
+ )}
461
+
462
+ {/* Terminal */}
463
+ <div
464
+ ref={containerRef}
465
+ className="flex-1 bg-[#0d0f14]"
466
+ style={{ minHeight: '300px', maxHeight }}
467
+ onClick={() => terminalRef.current?.focus()}
468
+ />
469
+
470
+ {/* Footer with actions */}
471
+ <div className="flex items-center justify-between px-4 py-3 border-t border-border-subtle bg-bg-tertiary">
472
+ <p className="text-xs text-text-muted">
473
+ Respond to prompts above to complete setup
474
+ </p>
475
+ <button
476
+ onClick={handleComplete}
477
+ className="px-4 py-2 bg-accent-cyan text-bg-deep text-sm font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors"
478
+ >
479
+ Done - Continue
480
+ </button>
481
+ </div>
482
+ </>
483
+ )}
484
+
485
+ </div>
486
+ );
487
+ }
488
+
489
+ // Icons
490
+ function CloseIcon() {
491
+ return (
492
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
493
+ <line x1="18" y1="6" x2="6" y2="18" />
494
+ <line x1="6" y1="6" x2="18" y2="18" />
495
+ </svg>
496
+ );
497
+ }
498
+
499
+ function AlertIcon() {
500
+ return (
501
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
502
+ <circle cx="12" cy="12" r="10" />
503
+ <line x1="12" y1="8" x2="12" y2="12" />
504
+ <line x1="12" y1="16" x2="12.01" y2="16" />
505
+ </svg>
506
+ );
507
+ }
508
+
509
+ function CheckIcon({ className, style }: { className?: string; style?: React.CSSProperties }) {
510
+ return (
511
+ <svg className={className} style={style} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
512
+ <polyline points="20 6 9 17 4 12" />
513
+ </svg>
514
+ );
515
+ }
516
+
517
+ export default TerminalProviderSetup;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ThemeProvider Component
3
+ *
4
+ * Provides theme context for light/dark mode support.
5
+ * Handles system preference detection and persistence.
6
+ *
7
+ * Note: Theme colors are defined as CSS variables in globals.css
8
+ * and referenced by Tailwind config. This enables automatic theme
9
+ * switching without duplicate style definitions.
10
+ */
11
+
12
+ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
13
+
14
+ export type Theme = 'light' | 'dark' | 'system';
15
+ export type ResolvedTheme = 'light' | 'dark';
16
+
17
+ export interface ThemeContextValue {
18
+ theme: Theme;
19
+ resolvedTheme: ResolvedTheme;
20
+ setTheme: (theme: Theme) => void;
21
+ toggleTheme: () => void;
22
+ }
23
+
24
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
25
+
26
+ export interface ThemeProviderProps {
27
+ children: React.ReactNode;
28
+ defaultTheme?: Theme;
29
+ storageKey?: string;
30
+ }
31
+
32
+ export function ThemeProvider({
33
+ children,
34
+ defaultTheme = 'system',
35
+ storageKey = 'dashboard-theme',
36
+ }: ThemeProviderProps) {
37
+ const [theme, setThemeState] = useState<Theme>(() => {
38
+ // Try to get from storage
39
+ if (typeof window !== 'undefined') {
40
+ const stored = localStorage.getItem(storageKey);
41
+ if (stored === 'light' || stored === 'dark' || stored === 'system') {
42
+ return stored;
43
+ }
44
+ }
45
+ return defaultTheme;
46
+ });
47
+
48
+ const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() => {
49
+ if (typeof window !== 'undefined') {
50
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
51
+ }
52
+ return 'light';
53
+ });
54
+
55
+ // Listen for system theme changes
56
+ useEffect(() => {
57
+ if (typeof window === 'undefined') return;
58
+
59
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
60
+ const handleChange = (e: MediaQueryListEvent) => {
61
+ setSystemTheme(e.matches ? 'dark' : 'light');
62
+ };
63
+
64
+ mediaQuery.addEventListener('change', handleChange);
65
+ return () => mediaQuery.removeEventListener('change', handleChange);
66
+ }, []);
67
+
68
+ // Resolve theme
69
+ const resolvedTheme: ResolvedTheme = theme === 'system' ? systemTheme : theme;
70
+
71
+ // Apply theme to document
72
+ useEffect(() => {
73
+ if (typeof document === 'undefined') return;
74
+
75
+ const root = document.documentElement;
76
+ root.classList.remove('theme-light', 'theme-dark');
77
+ root.classList.add(`theme-${resolvedTheme}`);
78
+ root.style.colorScheme = resolvedTheme;
79
+ }, [resolvedTheme]);
80
+
81
+ // Set theme with persistence
82
+ const setTheme = useCallback(
83
+ (newTheme: Theme) => {
84
+ setThemeState(newTheme);
85
+ if (typeof localStorage !== 'undefined') {
86
+ localStorage.setItem(storageKey, newTheme);
87
+ }
88
+ },
89
+ [storageKey]
90
+ );
91
+
92
+ // Toggle between light and dark
93
+ const toggleTheme = useCallback(() => {
94
+ setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
95
+ }, [resolvedTheme, setTheme]);
96
+
97
+ const value = useMemo(
98
+ () => ({ theme, resolvedTheme, setTheme, toggleTheme }),
99
+ [theme, resolvedTheme, setTheme, toggleTheme]
100
+ );
101
+
102
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
103
+ }
104
+
105
+ export function useTheme() {
106
+ const context = useContext(ThemeContext);
107
+ if (!context) {
108
+ throw new Error('useTheme must be used within a ThemeProvider');
109
+ }
110
+ return context;
111
+ }
112
+
113
+ /**
114
+ * ThemeToggle Component
115
+ * A simple button to toggle between themes
116
+ */
117
+ export interface ThemeToggleProps {
118
+ showLabel?: boolean;
119
+ className?: string;
120
+ }
121
+
122
+ export function ThemeToggle({ showLabel = false, className = '' }: ThemeToggleProps) {
123
+ const { resolvedTheme, toggleTheme } = useTheme();
124
+
125
+ return (
126
+ <button
127
+ className={`flex items-center gap-2 p-2 bg-bg-tertiary border border-border rounded-lg text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-all text-sm ${className}`}
128
+ onClick={toggleTheme}
129
+ aria-label={`Switch to ${resolvedTheme === 'light' ? 'dark' : 'light'} mode`}
130
+ >
131
+ {resolvedTheme === 'light' ? <MoonIcon /> : <SunIcon />}
132
+ {showLabel && <span>{resolvedTheme === 'light' ? 'Dark' : 'Light'} mode</span>}
133
+ </button>
134
+ );
135
+ }
136
+
137
+ function SunIcon() {
138
+ return (
139
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
140
+ <circle cx="12" cy="12" r="5" />
141
+ <line x1="12" y1="1" x2="12" y2="3" />
142
+ <line x1="12" y1="21" x2="12" y2="23" />
143
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
144
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
145
+ <line x1="1" y1="12" x2="3" y2="12" />
146
+ <line x1="21" y1="12" x2="23" y2="12" />
147
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
148
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
149
+ </svg>
150
+ );
151
+ }
152
+
153
+ function MoonIcon() {
154
+ return (
155
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
156
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
157
+ </svg>
158
+ );
159
+ }