@agent-relay/dashboard 2.0.82 → 2.0.84

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 (228) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
  3. package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
  4. package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
  5. package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
  6. package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
  7. package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
  8. package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
  9. package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
  10. package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
  11. package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
  12. package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
  13. package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
  14. package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
  15. package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
  16. package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
  17. package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
  18. package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
  19. package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
  20. package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
  21. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
  22. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
  23. package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
  24. package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
  25. package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
  26. package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
  27. package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
  28. package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
  29. package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
  30. package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
  31. package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
  32. package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
  33. package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
  34. package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
  35. package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
  36. package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
  37. package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
  38. package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
  39. package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
  40. package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
  41. package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
  42. package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
  43. package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
  44. package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
  45. package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
  46. package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
  47. package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
  48. package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
  49. package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
  50. package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
  51. package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
  52. package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
  53. package/out/about.html +2 -2
  54. package/out/about.txt +2 -2
  55. package/out/app/onboarding.html +1 -1
  56. package/out/app/onboarding.txt +2 -2
  57. package/out/app.html +1 -1
  58. package/out/app.txt +2 -2
  59. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
  60. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  61. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  62. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  63. package/out/blog.html +2 -2
  64. package/out/blog.txt +1 -1
  65. package/out/careers.html +2 -2
  66. package/out/careers.txt +2 -2
  67. package/out/changelog.html +2 -2
  68. package/out/changelog.txt +2 -2
  69. package/out/cloud/link.html +1 -1
  70. package/out/cloud/link.txt +2 -2
  71. package/out/complete-profile.html +2 -2
  72. package/out/complete-profile.txt +2 -2
  73. package/out/connect-repos.html +1 -1
  74. package/out/connect-repos.txt +2 -2
  75. package/out/contact.html +2 -2
  76. package/out/contact.txt +2 -2
  77. package/out/dev/cli-tools.html +1 -0
  78. package/out/dev/cli-tools.txt +7 -0
  79. package/out/dev/log-viewer.html +23 -0
  80. package/out/dev/log-viewer.txt +7 -0
  81. package/out/docs.html +2 -2
  82. package/out/docs.txt +2 -2
  83. package/out/history.html +1 -1
  84. package/out/history.txt +2 -2
  85. package/out/index.html +1 -1
  86. package/out/index.txt +2 -2
  87. package/out/login.html +2 -2
  88. package/out/login.txt +2 -2
  89. package/out/metrics.html +1 -1
  90. package/out/metrics.txt +2 -2
  91. package/out/pricing.html +2 -2
  92. package/out/pricing.txt +2 -2
  93. package/out/privacy.html +2 -2
  94. package/out/privacy.txt +2 -2
  95. package/out/providers/setup/claude.html +1 -1
  96. package/out/providers/setup/claude.txt +2 -2
  97. package/out/providers/setup/codex.html +1 -1
  98. package/out/providers/setup/codex.txt +2 -2
  99. package/out/providers/setup/cursor.html +1 -1
  100. package/out/providers/setup/cursor.txt +2 -2
  101. package/out/providers.html +1 -1
  102. package/out/providers.txt +2 -2
  103. package/out/security.html +2 -2
  104. package/out/security.txt +2 -2
  105. package/out/signup.html +2 -2
  106. package/out/signup.txt +2 -2
  107. package/out/terms.html +2 -2
  108. package/out/terms.txt +2 -2
  109. package/package.json +5 -1
  110. package/src/adapters/DashboardConfigProvider.tsx +56 -0
  111. package/src/adapters/cloudFetchAdapter.ts +278 -0
  112. package/src/adapters/index.ts +3 -0
  113. package/src/adapters/types.ts +508 -0
  114. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
  115. package/src/app/app/onboarding/page.tsx +870 -170
  116. package/src/app/cloud/link/page.tsx +14 -6
  117. package/src/app/connect-repos/page.tsx +9 -3
  118. package/src/app/dev/cli-tools/page.tsx +130 -0
  119. package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
  120. package/src/app/dev/log-viewer/fixtures.ts +110 -0
  121. package/src/app/dev/log-viewer/page.tsx +288 -0
  122. package/src/app/history/page.tsx +28 -12
  123. package/src/app/page.tsx +1 -1
  124. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
  125. package/src/components/AgentCard.tsx +4 -4
  126. package/src/components/AgentLogPreview.tsx +2 -38
  127. package/src/components/App.tsx +441 -2624
  128. package/src/components/CliToolHarness.test.tsx +83 -0
  129. package/src/components/CliToolHarness.tsx +292 -0
  130. package/src/components/CoordinatorPanel.tsx +13 -6
  131. package/src/components/LogViewer.tsx +2 -42
  132. package/src/components/ProviderAuthFlow.tsx +201 -81
  133. package/src/components/ProvisioningProgress.tsx +1 -1
  134. package/src/components/ReactionChips.tsx +2 -1
  135. package/src/components/SpawnModal.test.tsx +51 -18
  136. package/src/components/SpawnModal.tsx +175 -207
  137. package/src/components/TerminalProviderSetup.tsx +1 -1
  138. package/src/components/ThreadPanel.tsx +2 -0
  139. package/src/components/WorkspaceContext.tsx +7 -19
  140. package/src/components/XTermLogViewer.tsx +190 -27
  141. package/src/components/channels/ChannelMessageList.tsx +94 -4
  142. package/src/components/channels/ChannelViewV1.tsx +35 -11
  143. package/src/components/channels/api.ts +21 -20
  144. package/src/components/channels/types.ts +16 -0
  145. package/src/components/hooks/index.ts +0 -19
  146. package/src/components/hooks/useMessages.test.ts +80 -0
  147. package/src/components/hooks/useMessages.ts +13 -4
  148. package/src/components/hooks/useOrchestrator.ts +1 -1
  149. package/src/components/hooks/usePresence.ts +45 -6
  150. package/src/components/hooks/useThread.ts +83 -46
  151. package/src/components/hooks/useTrajectory.ts +62 -5
  152. package/src/components/hooks/useWebSocket.test.ts +358 -0
  153. package/src/components/hooks/useWebSocket.ts +243 -5
  154. package/src/components/index.ts +2 -14
  155. package/src/components/layout/Header.tsx +9 -15
  156. package/src/components/layout/Sidebar.tsx +1 -8
  157. package/src/components/settings/SettingsPage.tsx +108 -47
  158. package/src/components/settings/index.ts +0 -3
  159. package/src/landing/blogData.ts +1 -1
  160. package/src/lib/agent-merge.test.ts +2 -2
  161. package/src/lib/api.ts +8 -38
  162. package/src/lib/identity.test.ts +139 -0
  163. package/src/lib/identity.ts +48 -0
  164. package/src/lib/relaycastMessageAdapters.test.ts +182 -0
  165. package/src/lib/relaycastMessageAdapters.ts +105 -0
  166. package/src/lib/sanitize-logs.test.ts +227 -0
  167. package/src/lib/sanitize-logs.ts +202 -0
  168. package/src/providers/AgentProvider.tsx +799 -0
  169. package/src/providers/ChannelProvider.tsx +528 -0
  170. package/src/providers/CloudWorkspaceProvider.tsx +402 -0
  171. package/src/providers/MessageProvider.tsx +875 -0
  172. package/src/providers/RelayConfigProvider.tsx +94 -0
  173. package/src/providers/SendProvider.tsx +497 -0
  174. package/src/providers/SettingsProvider.tsx +247 -0
  175. package/src/providers/index.ts +26 -0
  176. package/src/types/index.ts +10 -10
  177. package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
  178. package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
  179. package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
  180. package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
  181. package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
  182. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
  183. package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
  184. package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
  185. package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
  186. package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
  187. package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
  188. package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
  189. package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
  190. package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
  191. package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
  192. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
  193. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
  194. package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
  195. package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
  196. package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
  197. package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
  198. package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
  199. package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
  200. package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
  201. package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
  202. package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
  203. package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
  204. package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
  205. package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
  206. package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
  207. package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
  208. package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
  209. package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
  210. package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
  211. package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
  212. package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
  213. package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
  214. package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
  215. package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
  216. package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
  217. package/src/components/BillingResult.tsx +0 -447
  218. package/src/components/CloudSessionProvider.tsx +0 -130
  219. package/src/components/SessionExpiredModal.tsx +0 -128
  220. package/src/components/WorkspaceStatusIndicator.tsx +0 -396
  221. package/src/components/hooks/useSession.ts +0 -209
  222. package/src/components/hooks/useWorkspaceMembers.ts +0 -132
  223. package/src/components/hooks/useWorkspaceStatus.ts +0 -237
  224. package/src/components/settings/BillingSettingsPanel.tsx +0 -564
  225. package/src/components/settings/TeamSettingsPanel.tsx +0 -560
  226. package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
  227. package/src/lib/cloudApi.ts +0 -893
  228. /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_ssgManifest.js +0 -0
@@ -2,18 +2,10 @@
2
2
  * Provider Auth Flow Component
3
3
  *
4
4
  * Shared component for AI provider authentication via SSH tunnel.
5
- * Used by both the onboarding page and workspace settings.
6
- *
7
- * Flow:
8
- * 1. Calls /api/auth/ssh/init to get a CLI command with one-time token
9
- * 2. User copies and runs the command in their local terminal
10
- * 3. CLI establishes SSH to workspace and runs the provider's auth command
11
- * 4. User completes interactive auth (OAuth in browser, etc.)
12
- * 5. CLI calls /api/auth/ssh/complete to mark provider as connected
13
- * 6. Dashboard polls /api/auth/ssh/status/:workspaceId to detect completion
5
+ * Supports both workspace mode and onboarding mode.
14
6
  */
15
7
 
16
- import React, { useState, useCallback, useRef, useEffect } from 'react';
8
+ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
17
9
 
18
10
  export interface ProviderInfo {
19
11
  id: string;
@@ -27,19 +19,38 @@ export interface ProviderInfo {
27
19
 
28
20
  export interface ProviderAuthFlowProps {
29
21
  provider: ProviderInfo;
30
- workspaceId: string;
22
+ workspaceId?: string;
23
+ mode?: 'workspace' | 'onboarding';
31
24
  csrfToken?: string;
25
+ showManualDone?: boolean;
32
26
  onSuccess: () => void;
33
27
  onCancel: () => void;
34
28
  onError: (error: string) => void;
35
29
  }
36
30
 
31
+ interface OnboardingNextStepResponse {
32
+ nextStep?: string;
33
+ connectedProviders?: string[];
34
+ }
35
+
36
+ async function getOnboardingNextStep(): Promise<OnboardingNextStepResponse> {
37
+ const response = await fetch('/api/onboarding/next-step', {
38
+ credentials: 'include',
39
+ });
40
+
41
+ if (!response.ok) {
42
+ throw new Error('Failed to fetch onboarding next step');
43
+ }
44
+
45
+ return (await response.json()) as OnboardingNextStepResponse;
46
+ }
47
+
37
48
  type AuthStatus = 'idle' | 'starting' | 'waiting' | 'success' | 'error';
38
49
 
39
50
  /**
40
51
  * Map dashboard provider IDs to SSH backend provider names.
41
52
  * The SSH status endpoint uses PROVIDER_COMMANDS keys (anthropic, openai, google, etc.)
42
- * while the dashboard uses its own IDs (anthropic, codex, google, etc.)
53
+ * while the dashboard may use codex for OpenAI.
43
54
  */
44
55
  const PROVIDER_STATUS_MAP: Record<string, string> = {
45
56
  codex: 'openai',
@@ -49,10 +60,21 @@ function getStatusProviderName(providerId: string): string {
49
60
  return PROVIDER_STATUS_MAP[providerId] || providerId;
50
61
  }
51
62
 
63
+ function getStatusProviderNames(providerId: string): string[] {
64
+ const mapped = getStatusProviderName(providerId);
65
+ return Array.from(new Set([mapped, providerId]));
66
+ }
67
+
68
+ function sleep(ms: number): Promise<void> {
69
+ return new Promise((resolve) => setTimeout(resolve, ms));
70
+ }
71
+
52
72
  export function ProviderAuthFlow({
53
73
  provider,
54
74
  workspaceId,
75
+ mode,
55
76
  csrfToken,
77
+ showManualDone,
56
78
  onSuccess,
57
79
  onCancel,
58
80
  onError,
@@ -61,11 +83,124 @@ export function ProviderAuthFlow({
61
83
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
62
84
  const [cliCommand, setCliCommand] = useState<string | null>(null);
63
85
  const [copied, setCopied] = useState(false);
86
+ const [statusWorkspaceId, setStatusWorkspaceId] = useState<string | null>(workspaceId || null);
87
+
64
88
  const pollingRef = useRef(false);
65
89
  const completingRef = useRef(false);
66
90
 
91
+ const effectiveMode = mode ?? (workspaceId ? 'workspace' : 'onboarding');
92
+ const shouldShowManualDone = showManualDone ?? effectiveMode === 'onboarding';
93
+
67
94
  const backendProviderId = provider.id;
68
- const statusProviderId = getStatusProviderName(backendProviderId);
95
+ const statusProviderNames = useMemo(
96
+ () => getStatusProviderNames(backendProviderId),
97
+ [backendProviderId]
98
+ );
99
+
100
+ const completeSuccess = useCallback(() => {
101
+ if (completingRef.current) {
102
+ return;
103
+ }
104
+
105
+ completingRef.current = true;
106
+ setStatus('success');
107
+ setTimeout(() => onSuccess(), 1200);
108
+ }, [onSuccess]);
109
+
110
+ const isProviderConnectedFromStatus = useCallback(
111
+ (providers: Array<{ name: string; status: string }> | undefined): boolean => {
112
+ if (!providers || !Array.isArray(providers)) {
113
+ return false;
114
+ }
115
+
116
+ return statusProviderNames.some((providerName) =>
117
+ providers.some((providerStatus) => providerStatus.name === providerName && providerStatus.status === 'connected')
118
+ );
119
+ },
120
+ [statusProviderNames]
121
+ );
122
+
123
+ const isProviderConnectedFromOnboarding = useCallback(
124
+ (data: OnboardingNextStepResponse): boolean => {
125
+ if (data.nextStep && data.nextStep !== 'connect_ai_provider') {
126
+ return true;
127
+ }
128
+
129
+ const connectedProviders = Array.isArray(data.connectedProviders)
130
+ ? data.connectedProviders
131
+ : [];
132
+
133
+ return statusProviderNames.some((providerName) => connectedProviders.includes(providerName));
134
+ },
135
+ [statusProviderNames]
136
+ );
137
+
138
+ const checkConnected = useCallback(
139
+ async (currentWorkspaceId?: string): Promise<boolean> => {
140
+ if (currentWorkspaceId) {
141
+ try {
142
+ const res = await fetch(`/api/auth/ssh/status/${currentWorkspaceId}`, {
143
+ credentials: 'include',
144
+ });
145
+
146
+ if (res.ok) {
147
+ const data = (await res.json()) as {
148
+ providers?: Array<{ name: string; status: string }>;
149
+ };
150
+
151
+ if (isProviderConnectedFromStatus(data.providers)) {
152
+ return true;
153
+ }
154
+ }
155
+ } catch {
156
+ // Fall through to onboarding check
157
+ }
158
+ }
159
+
160
+ if (effectiveMode !== 'onboarding') {
161
+ return false;
162
+ }
163
+
164
+ try {
165
+ const stepData = (await getOnboardingNextStep()) as OnboardingNextStepResponse;
166
+ return isProviderConnectedFromOnboarding(stepData);
167
+ } catch {
168
+ return false;
169
+ }
170
+ },
171
+ [effectiveMode, isProviderConnectedFromOnboarding, isProviderConnectedFromStatus]
172
+ );
173
+
174
+ const pollUntilConnected = useCallback(
175
+ async (workspaceForStatus?: string | null): Promise<void> => {
176
+ if (pollingRef.current) {
177
+ return;
178
+ }
179
+
180
+ pollingRef.current = true;
181
+
182
+ const maxAttempts = 120; // 10 minutes at 5s intervals
183
+ let attempts = 0;
184
+
185
+ while (pollingRef.current && attempts < maxAttempts) {
186
+ const connected = await checkConnected(workspaceForStatus || undefined);
187
+ if (connected) {
188
+ pollingRef.current = false;
189
+ completeSuccess();
190
+ return;
191
+ }
192
+
193
+ attempts += 1;
194
+ await sleep(5000);
195
+ }
196
+
197
+ pollingRef.current = false;
198
+ setErrorMessage('Authentication timed out. Please try again.');
199
+ setStatus('error');
200
+ onError('Authentication timed out');
201
+ },
202
+ [checkConnected, completeSuccess, onError]
203
+ );
69
204
 
70
205
  // Start the SSH auth flow
71
206
  const startAuth = useCallback(async () => {
@@ -75,99 +210,75 @@ export function ProviderAuthFlow({
75
210
 
76
211
  try {
77
212
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
78
- if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
213
+ if (csrfToken) {
214
+ headers['X-CSRF-Token'] = csrfToken;
215
+ }
216
+
217
+ const payload: Record<string, unknown> = {
218
+ provider: backendProviderId,
219
+ };
220
+
221
+ if (workspaceId) {
222
+ payload.workspaceId = workspaceId;
223
+ }
224
+
225
+ if (effectiveMode === 'onboarding') {
226
+ payload.mode = 'onboarding';
227
+ }
79
228
 
80
229
  const res = await fetch('/api/auth/ssh/init', {
81
230
  method: 'POST',
82
231
  credentials: 'include',
83
232
  headers,
84
- body: JSON.stringify({
85
- provider: backendProviderId,
86
- workspaceId,
87
- }),
233
+ body: JSON.stringify(payload),
88
234
  });
89
235
 
90
- const data = await res.json();
236
+ const data = (await res.json()) as {
237
+ command?: string;
238
+ commandWithUrl?: string;
239
+ workspaceId?: string;
240
+ error?: string;
241
+ };
91
242
 
92
243
  if (!res.ok) {
93
244
  throw new Error(data.error || 'Failed to start authentication');
94
245
  }
95
246
 
96
247
  const rawCommand = data.commandWithUrl || data.command;
97
- // Prepend npx if not already present
98
248
  const commandWithNpx = rawCommand && !rawCommand.trim().startsWith('npx ')
99
249
  ? `npx ${rawCommand}`
100
250
  : rawCommand;
251
+
252
+ if (!commandWithNpx) {
253
+ throw new Error('Auth command missing in response');
254
+ }
255
+
101
256
  setCliCommand(commandWithNpx);
102
257
  setStatus('waiting');
103
258
 
104
- // Start polling for completion
105
- if (data.workspaceId) {
106
- startPolling(data.workspaceId);
107
- }
259
+ const resolvedWorkspaceId = data.workspaceId || workspaceId || null;
260
+ setStatusWorkspaceId(resolvedWorkspaceId);
261
+
262
+ void pollUntilConnected(resolvedWorkspaceId);
108
263
  } catch (err) {
109
264
  const msg = err instanceof Error ? err.message : 'Failed to start authentication';
110
265
  setErrorMessage(msg);
111
266
  setStatus('error');
112
267
  onError(msg);
113
268
  }
114
- }, [backendProviderId, workspaceId, csrfToken, onError]);
115
-
116
- // Poll SSH auth status endpoint to detect when provider is connected
117
- const startPolling = useCallback((wsId: string) => {
118
- if (pollingRef.current) return;
119
- pollingRef.current = true;
120
-
121
- const maxAttempts = 120; // 10 minutes at 5s intervals
122
- let attempts = 0;
123
-
124
- const poll = async () => {
125
- if (attempts >= maxAttempts || !pollingRef.current) {
126
- pollingRef.current = false;
127
- if (attempts >= maxAttempts) {
128
- setErrorMessage('Authentication timed out. Please try again.');
129
- setStatus('error');
130
- onError('Authentication timed out');
131
- }
132
- return;
133
- }
269
+ }, [backendProviderId, workspaceId, effectiveMode, csrfToken, onError, pollUntilConnected]);
134
270
 
135
- try {
136
- const res = await fetch(`/api/auth/ssh/status/${wsId}`, {
137
- credentials: 'include',
138
- });
139
-
140
- if (res.ok) {
141
- const data = await res.json() as {
142
- providers: Array<{ name: string; status: string }>;
143
- };
144
-
145
- const providerStatus = data.providers?.find(
146
- (p) => p.name === statusProviderId
147
- );
148
-
149
- if (providerStatus?.status === 'connected') {
150
- pollingRef.current = false;
151
- if (!completingRef.current) {
152
- completingRef.current = true;
153
- setStatus('success');
154
- setTimeout(() => onSuccess(), 1500);
155
- }
156
- return;
157
- }
158
- }
159
-
160
- attempts++;
161
- setTimeout(poll, 5000);
162
- } catch (err) {
163
- console.error('Poll error:', err);
164
- attempts++;
165
- setTimeout(poll, 5000);
166
- }
167
- };
271
+ const handleManualDone = useCallback(async () => {
272
+ const connected = await checkConnected(statusWorkspaceId || undefined);
273
+ if (connected) {
274
+ completeSuccess();
275
+ return;
276
+ }
168
277
 
169
- poll();
170
- }, [statusProviderId, onError, onSuccess]);
278
+ const message = 'Authentication is still in progress. Finish the CLI flow and try again.';
279
+ setErrorMessage(message);
280
+ onError(message);
281
+ }, [checkConnected, statusWorkspaceId, completeSuccess, onError]);
171
282
 
172
283
  // Cancel auth flow
173
284
  const handleCancel = useCallback(() => {
@@ -191,7 +302,7 @@ export function ProviderAuthFlow({
191
302
  // Start auth when component mounts
192
303
  useEffect(() => {
193
304
  if (status === 'idle') {
194
- startAuth();
305
+ void startAuth();
195
306
  }
196
307
  }, [startAuth, status]);
197
308
 
@@ -281,7 +392,7 @@ export function ProviderAuthFlow({
281
392
  <strong>Step 4:</strong> Wait for the {provider.displayName} input prompt, then type <code className="px-1 py-0.5 bg-bg-deep rounded">exit</code>
282
393
  </p>
283
394
  <p className="text-xs text-amber-400/80">
284
- Do NOT close the terminal early. After sign-in completes, wait until you see the {provider.displayName} input screen (e.g. the <code className="px-1 py-0.5 bg-bg-deep rounded">&gt;</code> prompt). Then type <code className="px-1 py-0.5 bg-bg-deep rounded">exit</code> and press Enter. This page will update automatically.
395
+ Do not close the terminal early. After sign-in completes, wait until you see the input screen prompt, then type <code className="px-1 py-0.5 bg-bg-deep rounded">exit</code> and press Enter.
285
396
  </p>
286
397
  </div>
287
398
 
@@ -294,6 +405,15 @@ export function ProviderAuthFlow({
294
405
  <span>Waiting for authentication to complete...</span>
295
406
  </div>
296
407
 
408
+ {shouldShowManualDone && (
409
+ <button
410
+ onClick={handleManualDone}
411
+ className="w-full py-2 px-4 bg-accent-cyan text-bg-deep font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors"
412
+ >
413
+ Done
414
+ </button>
415
+ )}
416
+
297
417
  {/* Cancel button */}
298
418
  <button
299
419
  onClick={handleCancel}
@@ -77,7 +77,7 @@ const TERMINAL_MESSAGES = [
77
77
  '> Establishing secure connection...',
78
78
  '> Allocating compute resources...',
79
79
  '> Configuring agent protocols...',
80
- '> Initializing relay daemon...',
80
+ '> Initializing relay broker...',
81
81
  '> Syncing workspace state...',
82
82
  '> Warming up inference engine...',
83
83
  '> Connecting to neural mesh...',
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useRef } from 'react';
2
+ import { resolveEmoji } from '@relaycast/types';
2
3
  import type { Reaction } from '../types';
3
4
  import { ReactionPicker } from './ReactionPicker';
4
5
 
@@ -40,7 +41,7 @@ export function ReactionChips({
40
41
  }
41
42
  `}
42
43
  >
43
- <span>{reaction.emoji}</span>
44
+ <span>{resolveEmoji(reaction.emoji)}</span>
44
45
  <span className="font-medium">{reaction.count}</span>
45
46
  </button>
46
47
  );
@@ -10,24 +10,10 @@ import React from 'react';
10
10
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
11
  import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
12
12
  import { SpawnModal } from './SpawnModal';
13
+ import { DashboardConfigProvider } from '../adapters';
13
14
 
14
15
  const MOCK_WORKSPACE_ID = '12345678-1234-1234-1234-123456789012';
15
16
 
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
17
  const mockRepos = [
32
18
  { id: 'repo-1', githubFullName: 'AgentWorkforce/relay' },
33
19
  { id: 'repo-2', githubFullName: 'AgentWorkforce/trajectories' },
@@ -40,15 +26,27 @@ function getForm(): HTMLFormElement {
40
26
  return form;
41
27
  }
42
28
 
43
- function renderSpawnModal(overrides: Partial<React.ComponentProps<typeof SpawnModal>> = {}) {
29
+ type SpawnModalOverrides = Partial<React.ComponentProps<typeof SpawnModal>> & {
30
+ isCloudMode?: boolean;
31
+ };
32
+
33
+ function renderSpawnModal(overrides: SpawnModalOverrides = {}) {
34
+ const { isCloudMode = false, ...spawnModalOverrides } = overrides;
44
35
  const defaultProps: React.ComponentProps<typeof SpawnModal> = {
45
36
  isOpen: true,
46
37
  onClose: vi.fn(),
47
38
  onSpawn: vi.fn().mockResolvedValue(true),
48
39
  existingAgents: [],
49
- ...overrides,
40
+ ...spawnModalOverrides,
41
+ };
42
+ return {
43
+ ...render(
44
+ <DashboardConfigProvider config={{ features: { workspaces: isCloudMode } }}>
45
+ <SpawnModal {...defaultProps} />
46
+ </DashboardConfigProvider>
47
+ ),
48
+ props: defaultProps,
50
49
  };
51
- return { ...render(<SpawnModal {...defaultProps} />), props: defaultProps };
52
50
  }
53
51
 
54
52
  describe('SpawnModal', () => {
@@ -157,6 +155,41 @@ describe('SpawnModal', () => {
157
155
  });
158
156
  });
159
157
 
158
+ describe('resume previous session', () => {
159
+ it('includes continueFrom when toggle is enabled', async () => {
160
+ const onSpawn = vi.fn().mockResolvedValue(true);
161
+ renderSpawnModal({ onSpawn });
162
+
163
+ // Enable the resume toggle
164
+ const resumeSection = screen.getByText('Resume Previous Session');
165
+ const toggle = resumeSection.closest('div')?.parentElement?.querySelector('button[aria-pressed]');
166
+ if (toggle) fireEvent.click(toggle);
167
+
168
+ fireEvent.submit(getForm());
169
+
170
+ await waitFor(() => {
171
+ expect(onSpawn).toHaveBeenCalled();
172
+ });
173
+
174
+ const config = onSpawn.mock.calls[0][0];
175
+ expect(config.continueFrom).toBe('claude-1'); // default suggested name
176
+ });
177
+
178
+ it('does not include continueFrom when toggle is disabled', async () => {
179
+ const onSpawn = vi.fn().mockResolvedValue(true);
180
+ renderSpawnModal({ onSpawn });
181
+
182
+ fireEvent.submit(getForm());
183
+
184
+ await waitFor(() => {
185
+ expect(onSpawn).toHaveBeenCalled();
186
+ });
187
+
188
+ const config = onSpawn.mock.calls[0][0];
189
+ expect(config.continueFrom).toBeUndefined();
190
+ });
191
+ });
192
+
160
193
  describe('working directory (local mode)', () => {
161
194
  it('shows working directory input when not in cloud mode', () => {
162
195
  renderSpawnModal({ isCloudMode: false });