@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
@@ -1,6 +1,12 @@
1
1
  import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import {
3
+ useThread as useRelayThread,
4
+ useReply as useRelayReply,
5
+ } from '@relaycast/react';
6
+ import type { MessageWithMeta } from '@relaycast/sdk';
2
7
  import type { Message } from '../../types';
3
8
  import { api } from '../../lib/api';
9
+ import { useRelayConfigStatus } from '../../providers/RelayConfigProvider';
4
10
 
5
11
  interface UseThreadOptions {
6
12
  threadId: string | null;
@@ -19,118 +25,149 @@ interface UseThreadReturn {
19
25
  addReply: (reply: Message) => void;
20
26
  }
21
27
 
28
+ function toMessage(msg: MessageWithMeta): Message {
29
+ return {
30
+ id: msg.id,
31
+ from: msg.agentName ?? '',
32
+ to: '*',
33
+ content: msg.text,
34
+ timestamp: msg.createdAt ?? new Date().toISOString(),
35
+ replyCount: msg.replyCount ?? 0,
36
+ reactions: msg.reactions ?? [],
37
+ isRead: true,
38
+ };
39
+ }
40
+
22
41
  export function useThread({ threadId, fallbackMessages }: UseThreadOptions): UseThreadReturn {
23
- const [parentMessage, setParentMessage] = useState<Message | null>(null);
24
- const [replies, setReplies] = useState<Message[]>([]);
25
- const [isLoading, setIsLoading] = useState(false);
42
+ const { configured: relayConfigured } = useRelayConfigStatus();
43
+
44
+ // SDK hooks always called (hooks can't be conditional)
45
+ const relayThread = useRelayThread(threadId ?? '');
46
+ const { reply: relayReply } = useRelayReply();
47
+
48
+ // REST / client-side fallback state
49
+ const [restParent, setRestParent] = useState<Message | null>(null);
50
+ const [restReplies, setRestReplies] = useState<Message[]>([]);
51
+ const [restLoading, setRestLoading] = useState(false);
26
52
  const [hasMore, setHasMore] = useState(false);
27
53
  const [cursor, setCursor] = useState<string | undefined>();
28
54
  const [useFallback, setUseFallback] = useState(false);
29
-
30
- // Use a ref to track the active threadId for cancellation of loadMore
31
55
  const activeThreadIdRef = useRef<string | null>(null);
32
56
 
33
- // Fetch thread from API when threadId changes
57
+ // REST / fallback fetch when Relaycast is not configured
34
58
  useEffect(() => {
35
59
  activeThreadIdRef.current = threadId;
36
-
37
- if (!threadId) {
38
- setParentMessage(null);
39
- setReplies([]);
60
+ if (!threadId || relayConfigured) {
61
+ setRestParent(null);
62
+ setRestReplies([]);
40
63
  setHasMore(false);
41
64
  setCursor(undefined);
42
65
  setUseFallback(false);
43
- setIsLoading(false);
66
+ setRestLoading(false);
44
67
  return;
45
68
  }
46
69
 
47
- // Reset state immediately when switching threads to avoid stale data flash
48
- setParentMessage(null);
49
- setReplies([]);
70
+ setRestParent(null);
71
+ setRestReplies([]);
50
72
  setHasMore(false);
51
73
  setCursor(undefined);
52
74
  setUseFallback(false);
53
75
 
54
76
  let cancelled = false;
55
- setIsLoading(true);
77
+ setRestLoading(true);
56
78
 
57
- api.getThread(threadId, { limit: 50 }).then((result) => {
79
+ const loadThread = async () => {
80
+ const result = await api.getThread(threadId, { limit: 50 });
58
81
  if (cancelled) return;
59
- setIsLoading(false);
82
+ setRestLoading(false);
60
83
 
61
84
  if (result.success && result.data) {
62
85
  setUseFallback(false);
63
86
  const { parent, replies: fetchedReplies, nextCursor } = result.data;
64
- setParentMessage(parent as Message);
65
- setReplies(fetchedReplies);
87
+ setRestParent(parent as Message);
88
+ setRestReplies(fetchedReplies);
66
89
  setHasMore(!!nextCursor);
67
90
  setCursor(nextCursor);
68
91
  } else {
69
- // API not available — fall back to client-side messages
70
92
  setUseFallback(true);
71
93
  }
72
- });
73
-
74
- return () => {
75
- cancelled = true;
76
94
  };
77
- }, [threadId]);
78
95
 
79
- // Use fallback messages when API is unavailable
80
- // For topic threads, the threadId is not the id of any message — it's the `thread` field on replies.
81
- // So we also check for the first reply whose thread matches.
82
- const effectiveParent = useFallback
96
+ void loadThread();
97
+ return () => { cancelled = true; };
98
+ }, [threadId, relayConfigured]);
99
+
100
+ // --- Derive final values based on mode ---
101
+
102
+ const isRelayMode = relayConfigured && !!threadId;
103
+
104
+ // Relay path: map SDK MessageWithMeta → dashboard Message
105
+ const relayParent = isRelayMode && relayThread.parent ? toMessage(relayThread.parent) : null;
106
+ const relayReplies = isRelayMode ? relayThread.replies.map(toMessage) : [];
107
+
108
+ // Fallback path: use client-side messages
109
+ const fallbackParent = useFallback
83
110
  ? (fallbackMessages?.find((m) => m.id === threadId)
84
111
  ?? fallbackMessages?.filter((m) => m.thread === threadId)
85
112
  .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())[0]
86
113
  ?? null)
87
- : parentMessage;
114
+ : null;
115
+ const fallbackReplies = useFallback
116
+ ? (fallbackMessages?.filter((m) => m.thread === threadId && m.id !== fallbackParent?.id) ?? [])
117
+ : [];
88
118
 
89
- const effectiveReplies = useFallback
90
- ? (fallbackMessages?.filter((m) => m.thread === threadId && m.id !== effectiveParent?.id) ?? [])
91
- : replies;
119
+ // Pick the active source
120
+ const parentMessage = isRelayMode ? relayParent : (useFallback ? fallbackParent : restParent);
121
+ const replies = isRelayMode ? relayReplies : (useFallback ? fallbackReplies : restReplies);
122
+ const isLoading = isRelayMode ? relayThread.loading : restLoading;
92
123
 
93
124
  const loadMore = useCallback(async () => {
94
- if (!threadId || !hasMore || !cursor || useFallback) return;
125
+ if (relayConfigured || !threadId || !hasMore || !cursor || useFallback) return;
95
126
  const loadingThreadId = threadId;
96
- setIsLoading(true);
127
+ setRestLoading(true);
97
128
  const result = await api.getThread(threadId, { cursor, limit: 50 });
98
- // If thread changed while loading, discard the stale response
99
129
  if (activeThreadIdRef.current !== loadingThreadId) return;
100
- setIsLoading(false);
130
+ setRestLoading(false);
101
131
  if (result.success && result.data) {
102
- setReplies((prev) => [...result.data!.replies, ...prev]);
132
+ setRestReplies((prev) => [...result.data!.replies, ...prev]);
103
133
  setHasMore(!!result.data.nextCursor);
104
134
  setCursor(result.data.nextCursor);
105
135
  }
106
- }, [threadId, hasMore, cursor, useFallback]);
136
+ }, [threadId, hasMore, cursor, useFallback, relayConfigured]);
107
137
 
108
138
  const sendReply = useCallback(
109
139
  async (text: string): Promise<boolean> => {
110
140
  if (!threadId) return false;
141
+ if (relayConfigured) {
142
+ try {
143
+ await relayReply(threadId, text);
144
+ return true;
145
+ } catch {
146
+ // Fall through to REST fallback.
147
+ }
148
+ }
111
149
  const result = await api.postReply(threadId, text);
112
150
  if (result.success && result.data) {
113
- setReplies((prev) => [...prev, result.data!]);
151
+ setRestReplies((prev) => [...prev, result.data!]);
114
152
  return true;
115
153
  }
116
154
  return false;
117
155
  },
118
- [threadId],
156
+ [threadId, relayConfigured, relayReply],
119
157
  );
120
158
 
121
159
  const addReply = useCallback((reply: Message) => {
122
- setReplies((prev) => {
123
- // Deduplicate
160
+ setRestReplies((prev) => {
124
161
  if (prev.some((m) => m.id === reply.id)) return prev;
125
162
  return [...prev, reply];
126
163
  });
127
164
  }, []);
128
165
 
129
166
  return {
130
- parentMessage: effectiveParent,
131
- replies: effectiveReplies,
167
+ parentMessage,
168
+ replies,
132
169
  isLoading,
133
- hasMore: useFallback ? false : hasMore,
170
+ hasMore: useFallback || isRelayMode ? false : hasMore,
134
171
  loadMore,
135
172
  sendReply,
136
173
  addReply,
@@ -49,6 +49,61 @@ interface UseTrajectoryResult {
49
49
  selectedTrajectoryId: string | null;
50
50
  }
51
51
 
52
+ function extractHistory(data: unknown): { success: boolean; trajectories: TrajectoryHistoryEntry[]; error?: string } {
53
+ if (!data || typeof data !== 'object') {
54
+ return { success: false, trajectories: [], error: 'Invalid history response' };
55
+ }
56
+
57
+ const raw = data as { success?: boolean; trajectories?: TrajectoryHistoryEntry[] };
58
+ const hasSuccess = Object.prototype.hasOwnProperty.call(raw, 'success');
59
+
60
+ if (Array.isArray((data as { trajectories?: unknown }).trajectories)) {
61
+ const trajectories = (data as { trajectories: TrajectoryHistoryEntry[] }).trajectories;
62
+ const isSuccess = hasSuccess ? raw.success === true : true;
63
+ return { success: Boolean(isSuccess), trajectories: isSuccess ? trajectories : [] };
64
+ }
65
+
66
+ if (Array.isArray(data)) {
67
+ return { success: true, trajectories: data as TrajectoryHistoryEntry[] };
68
+ }
69
+
70
+ return {
71
+ success: false,
72
+ trajectories: [],
73
+ error: 'History payload missing trajectories',
74
+ };
75
+ }
76
+
77
+ function extractSteps(data: unknown): { success: boolean; steps: TrajectoryStep[]; error?: string } {
78
+ if (!data || typeof data !== 'object') {
79
+ return { success: false, steps: [], error: 'Invalid steps response' };
80
+ }
81
+
82
+ const raw = data as { success?: boolean; steps?: TrajectoryStep[]; error?: string };
83
+ const hasSuccess = Object.prototype.hasOwnProperty.call(raw, 'success');
84
+ const hasSteps = Array.isArray(raw.steps);
85
+
86
+ if (hasSteps) {
87
+ const isSuccess = hasSuccess ? raw.success === true : true;
88
+ const steps = raw.steps ?? [];
89
+ return {
90
+ success: Boolean(isSuccess),
91
+ steps: isSuccess ? steps : [],
92
+ error: !isSuccess ? raw.error : undefined,
93
+ };
94
+ }
95
+
96
+ if (Array.isArray(data)) {
97
+ return { success: true, steps: data as TrajectoryStep[] };
98
+ }
99
+
100
+ return {
101
+ success: false,
102
+ steps: [],
103
+ error: 'Steps payload missing steps',
104
+ };
105
+ }
106
+
52
107
  export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectoryResult {
53
108
  const {
54
109
  pollInterval = 2000,
@@ -103,9 +158,10 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
103
158
  : getApiUrl('/api/trajectory/history');
104
159
  const response = await fetch(url, { credentials: 'include' });
105
160
  const data = await response.json();
161
+ const normalized = extractHistory(data);
106
162
 
107
- if (data.success) {
108
- setHistory(data.trajectories || []);
163
+ if (normalized.success) {
164
+ setHistory(normalized.trajectories || []);
109
165
  }
110
166
  } catch (err: any) {
111
167
  console.error('[useTrajectory] History fetch error:', err);
@@ -129,6 +185,7 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
129
185
 
130
186
  const response = await fetch(url, { credentials: 'include' });
131
187
  const data = await response.json();
188
+ const normalized = extractSteps(data);
132
189
 
133
190
  // Only update state if this is still the most recent request
134
191
  // Check both request counter AND trajectory ID for double protection
@@ -141,11 +198,11 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
141
198
  return;
142
199
  }
143
200
 
144
- if (data.success) {
145
- setSteps(data.steps || []);
201
+ if (normalized.success) {
202
+ setSteps(normalized.steps || []);
146
203
  setError(null);
147
204
  } else {
148
- setError(data.error || 'Failed to fetch trajectory steps');
205
+ setError(normalized.error || 'Failed to fetch trajectory steps');
149
206
  }
150
207
  } catch (err: any) {
151
208
  // Only update error state if this is still the current request
@@ -0,0 +1,358 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { DashboardData } from './useWebSocket';
3
+ import { applyBrokerEvent } from './useWebSocket';
4
+
5
+ function emptyState(): DashboardData {
6
+ return {
7
+ agents: [],
8
+ messages: [],
9
+ };
10
+ }
11
+
12
+ describe('applyBrokerEvent', () => {
13
+ it('adds relay_inbound DM to messages', () => {
14
+ const next = applyBrokerEvent(emptyState(), {
15
+ kind: 'relay_inbound',
16
+ event_id: 'evt_1',
17
+ from: 'Dashboard',
18
+ target: 'Lead',
19
+ body: 'hello',
20
+ });
21
+
22
+ expect(next?.messages).toHaveLength(1);
23
+ expect(next?.messages[0]).toMatchObject({
24
+ id: 'evt_1',
25
+ from: 'Dashboard',
26
+ to: 'Lead',
27
+ content: 'hello',
28
+ });
29
+ });
30
+
31
+ it('skips relay_inbound channel messages (handled by useChannels)', () => {
32
+ const next = applyBrokerEvent(emptyState(), {
33
+ kind: 'relay_inbound',
34
+ event_id: 'evt_ch',
35
+ from: 'Hero',
36
+ target: '#general',
37
+ body: 'hello channel',
38
+ });
39
+
40
+ expect(next?.messages).toHaveLength(0);
41
+ });
42
+
43
+ it('bootstraps empty state when prev is null', () => {
44
+ const next = applyBrokerEvent(null, {
45
+ kind: 'relay_inbound',
46
+ event_id: 'evt_null',
47
+ from: 'Dashboard',
48
+ target: 'Lead',
49
+ body: 'hello',
50
+ });
51
+
52
+ expect(next).not.toBeNull();
53
+ expect(next?.messages).toHaveLength(1);
54
+ });
55
+
56
+ it('skips relay_inbound with missing fields', () => {
57
+ const next = applyBrokerEvent(emptyState(), {
58
+ kind: 'relay_inbound',
59
+ event_id: 'evt_bad',
60
+ from: '',
61
+ target: 'Lead',
62
+ body: 'hello',
63
+ });
64
+
65
+ expect(next?.messages).toHaveLength(0);
66
+ });
67
+
68
+ // --- Delivery Status ---
69
+
70
+ describe('delivery status events', () => {
71
+ it('delivery_verified sets message status to acked by event_id', () => {
72
+ const state: DashboardData = {
73
+ agents: [],
74
+ messages: [
75
+ { id: 'evt_1', from: 'Dashboard', to: 'Lead', content: 'hello', timestamp: '2024-01-01', status: 'sending' },
76
+ ],
77
+ };
78
+
79
+ const next = applyBrokerEvent(state, {
80
+ kind: 'delivery_verified',
81
+ name: 'Lead',
82
+ delivery_id: 'del_1',
83
+ event_id: 'evt_1',
84
+ });
85
+
86
+ expect(next?.messages[0]?.status).toBe('acked');
87
+ });
88
+
89
+ it('delivery_verified does not modify messages with different event_id', () => {
90
+ const state: DashboardData = {
91
+ agents: [],
92
+ messages: [
93
+ { id: 'evt_other', from: 'Dashboard', to: 'Lead', content: 'hello', timestamp: '2024-01-01', status: 'sending' },
94
+ ],
95
+ };
96
+
97
+ const next = applyBrokerEvent(state, {
98
+ kind: 'delivery_verified',
99
+ name: 'Lead',
100
+ delivery_id: 'del_1',
101
+ event_id: 'evt_1',
102
+ });
103
+
104
+ expect(next?.messages[0]?.status).toBe('sending');
105
+ });
106
+
107
+ it('delivery_verified with missing event_id returns prev state', () => {
108
+ const state = emptyState();
109
+ const next = applyBrokerEvent(state, {
110
+ kind: 'delivery_verified',
111
+ name: 'Lead',
112
+ delivery_id: 'del_1',
113
+ });
114
+ expect(next).toBe(state);
115
+ });
116
+
117
+ it('delivery_failed sets message status to failed by event_id', () => {
118
+ const state: DashboardData = {
119
+ agents: [],
120
+ messages: [
121
+ { id: 'evt_2', from: 'Dashboard', to: 'Lead', content: 'hello', timestamp: '2024-01-01', status: 'sending' },
122
+ ],
123
+ };
124
+
125
+ const next = applyBrokerEvent(state, {
126
+ kind: 'delivery_failed',
127
+ name: 'Lead',
128
+ delivery_id: 'del_2',
129
+ event_id: 'evt_2',
130
+ reason: 'timeout',
131
+ });
132
+
133
+ expect(next?.messages[0]?.status).toBe('failed');
134
+ });
135
+
136
+ it('delivery_failed with missing event_id returns prev state', () => {
137
+ const state = emptyState();
138
+ const next = applyBrokerEvent(state, {
139
+ kind: 'delivery_failed',
140
+ name: 'Lead',
141
+ delivery_id: 'del_2',
142
+ reason: 'timeout',
143
+ });
144
+ expect(next).toBe(state);
145
+ });
146
+ });
147
+
148
+ // --- Thinking / Processing State ---
149
+
150
+ describe('thinking/processing state events', () => {
151
+ function stateWithAgent(name: string): DashboardData {
152
+ return {
153
+ agents: [{ name, status: 'online' }],
154
+ messages: [],
155
+ };
156
+ }
157
+
158
+ it('delivery_ack sets agent isProcessing and processingStartedAt', () => {
159
+ const state = stateWithAgent('Lead');
160
+ const before = Date.now();
161
+
162
+ const next = applyBrokerEvent(state, {
163
+ kind: 'delivery_ack',
164
+ delivery_id: 'del_1',
165
+ name: 'Lead',
166
+ });
167
+
168
+ const agent = next?.agents.find((a) => a.name === 'Lead');
169
+ expect(agent?.isProcessing).toBe(true);
170
+ expect(agent?.processingStartedAt).toBeGreaterThanOrEqual(before);
171
+ expect(agent?.processingStartedAt).toBeLessThanOrEqual(Date.now());
172
+ });
173
+
174
+ it('delivery_active sets agent isProcessing and processingStartedAt', () => {
175
+ const state = stateWithAgent('Lead');
176
+ const before = Date.now();
177
+
178
+ const next = applyBrokerEvent(state, {
179
+ kind: 'delivery_active',
180
+ delivery_id: 'del_1',
181
+ name: 'Lead',
182
+ });
183
+
184
+ const agent = next?.agents.find((a) => a.name === 'Lead');
185
+ expect(agent?.isProcessing).toBe(true);
186
+ expect(agent?.processingStartedAt).toBeGreaterThanOrEqual(before);
187
+ });
188
+
189
+ it('delivery_ack with missing name field returns prev state', () => {
190
+ const state = stateWithAgent('Lead');
191
+ const next = applyBrokerEvent(state, {
192
+ kind: 'delivery_ack',
193
+ delivery_id: 'del_1',
194
+ });
195
+ expect(next).toBe(state);
196
+ });
197
+
198
+ it('delivery_active with missing name field returns prev state', () => {
199
+ const state = stateWithAgent('Lead');
200
+ const next = applyBrokerEvent(state, {
201
+ kind: 'delivery_active',
202
+ delivery_id: 'del_1',
203
+ });
204
+ expect(next).toBe(state);
205
+ });
206
+
207
+ it('delivery_ack does not modify unrelated agents', () => {
208
+ const state: DashboardData = {
209
+ agents: [
210
+ { name: 'Lead', status: 'online' },
211
+ { name: 'Helper', status: 'online' },
212
+ ],
213
+ messages: [],
214
+ };
215
+
216
+ const next = applyBrokerEvent(state, {
217
+ kind: 'delivery_ack',
218
+ delivery_id: 'del_1',
219
+ name: 'Lead',
220
+ });
221
+
222
+ expect(next?.agents.find((a) => a.name === 'Helper')?.isProcessing).toBeUndefined();
223
+ });
224
+
225
+ it('agent_idle clears processingStartedAt and lastLogLine', () => {
226
+ const state: DashboardData = {
227
+ agents: [{ name: 'Lead', status: 'online', isProcessing: true, processingStartedAt: 12345, lastLogLine: 'some log' }],
228
+ messages: [],
229
+ };
230
+
231
+ const next = applyBrokerEvent(state, {
232
+ kind: 'agent_idle',
233
+ name: 'Lead',
234
+ idle_secs: 5,
235
+ });
236
+
237
+ const agent = next?.agents.find((a) => a.name === 'Lead');
238
+ expect(agent?.isProcessing).toBe(false);
239
+ expect(agent?.processingStartedAt).toBeUndefined();
240
+ expect(agent?.lastLogLine).toBeUndefined();
241
+ });
242
+ });
243
+
244
+ // --- Worker Stream Logs ---
245
+
246
+ describe('worker stream events', () => {
247
+ it('worker_stream sets lastLogLine on the matching agent', () => {
248
+ const state: DashboardData = {
249
+ agents: [{ name: 'Lead', status: 'online', isProcessing: true }],
250
+ messages: [],
251
+ };
252
+
253
+ const next = applyBrokerEvent(state, {
254
+ kind: 'worker_stream',
255
+ name: 'Lead',
256
+ stream: 'stdout',
257
+ chunk: 'Running tests...',
258
+ });
259
+
260
+ expect(next?.agents.find((a) => a.name === 'Lead')?.lastLogLine).toBe('Running tests...');
261
+ });
262
+
263
+ it('worker_stream does not modify unrelated agents', () => {
264
+ const state: DashboardData = {
265
+ agents: [
266
+ { name: 'Lead', status: 'online' },
267
+ { name: 'Helper', status: 'online' },
268
+ ],
269
+ messages: [],
270
+ };
271
+
272
+ const next = applyBrokerEvent(state, {
273
+ kind: 'worker_stream',
274
+ name: 'Lead',
275
+ stream: 'stdout',
276
+ chunk: 'log line',
277
+ });
278
+
279
+ expect(next?.agents.find((a) => a.name === 'Helper')?.lastLogLine).toBeUndefined();
280
+ });
281
+
282
+ it('worker_stream with missing name returns prev state', () => {
283
+ const state = emptyState();
284
+ const next = applyBrokerEvent(state, {
285
+ kind: 'worker_stream',
286
+ stream: 'stdout',
287
+ chunk: 'text',
288
+ });
289
+ expect(next).toBe(state);
290
+ });
291
+
292
+ it('worker_stream overwrites previous lastLogLine', () => {
293
+ const state: DashboardData = {
294
+ agents: [{ name: 'Lead', status: 'online', lastLogLine: 'old line' }],
295
+ messages: [],
296
+ };
297
+
298
+ const next = applyBrokerEvent(state, {
299
+ kind: 'worker_stream',
300
+ name: 'Lead',
301
+ stream: 'stdout',
302
+ chunk: 'new line',
303
+ });
304
+
305
+ expect(next?.agents.find((a) => a.name === 'Lead')?.lastLogLine).toBe('new line');
306
+ });
307
+ });
308
+
309
+ // --- Full Lifecycle Integration ---
310
+
311
+ describe('full delivery lifecycle', () => {
312
+ it('sending -> acked -> processing -> stream -> idle clears all state', () => {
313
+ let state: DashboardData = {
314
+ agents: [{ name: 'Lead', status: 'online' }],
315
+ messages: [
316
+ { id: 'evt_lc', from: 'Dashboard', to: 'Lead', content: 'do stuff', timestamp: '2024-01-01', status: 'sending' },
317
+ ],
318
+ };
319
+
320
+ // 1. delivery_verified -> message becomes acked
321
+ state = applyBrokerEvent(state, {
322
+ kind: 'delivery_verified',
323
+ name: 'Lead',
324
+ delivery_id: 'del_lc',
325
+ event_id: 'evt_lc',
326
+ })!;
327
+ expect(state.messages[0]?.status).toBe('acked');
328
+
329
+ // 2. delivery_ack -> agent starts processing
330
+ state = applyBrokerEvent(state, {
331
+ kind: 'delivery_ack',
332
+ delivery_id: 'del_lc',
333
+ name: 'Lead',
334
+ })!;
335
+ expect(state.agents[0]?.isProcessing).toBe(true);
336
+ expect(state.agents[0]?.processingStartedAt).toBeDefined();
337
+
338
+ // 3. worker_stream -> log line appears
339
+ state = applyBrokerEvent(state, {
340
+ kind: 'worker_stream',
341
+ name: 'Lead',
342
+ stream: 'stdout',
343
+ chunk: 'Compiling...',
344
+ })!;
345
+ expect(state.agents[0]?.lastLogLine).toBe('Compiling...');
346
+
347
+ // 4. agent_idle -> processing state fully cleared
348
+ state = applyBrokerEvent(state, {
349
+ kind: 'agent_idle',
350
+ name: 'Lead',
351
+ idle_secs: 3,
352
+ })!;
353
+ expect(state.agents[0]?.isProcessing).toBe(false);
354
+ expect(state.agents[0]?.processingStartedAt).toBeUndefined();
355
+ expect(state.agents[0]?.lastLogLine).toBeUndefined();
356
+ });
357
+ });
358
+ });