@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
@@ -23,6 +23,19 @@ export interface XTermLogViewerProps {
23
23
  onClose?: () => void;
24
24
  /** Custom class name */
25
25
  className?: string;
26
+ /**
27
+ * Mock mode: provide lines directly instead of connecting to WebSocket.
28
+ * Each entry has `content` (raw PTY text) and an optional `delay` (ms)
29
+ * for streaming simulation.
30
+ */
31
+ mockData?: { content: string; delay?: number }[];
32
+ /** When true, feed mockData lines one at a time with their delays */
33
+ mockStreaming?: boolean;
34
+ /** Legacy flag kept for compatibility; real streams are now rendered losslessly. */
35
+ suppressNoisyOutput?: boolean;
36
+ // Accept legacy/extra props for isolated test harnesses while preserving
37
+ // compatibility across in-progress refactors.
38
+ [key: string]: unknown;
26
39
  }
27
40
 
28
41
  // Theme matching the dashboard dark theme
@@ -59,7 +72,11 @@ export function XTermLogViewer({
59
72
  showHeader = true,
60
73
  onClose,
61
74
  className = '',
75
+ mockData,
76
+ mockStreaming = false,
77
+ suppressNoisyOutput = false,
62
78
  }: XTermLogViewerProps) {
79
+ const isMockMode = !!mockData;
63
80
  const containerRef = useRef<HTMLDivElement>(null);
64
81
  const terminalRef = useRef<Terminal | null>(null);
65
82
  const fitAddonRef = useRef<FitAddon | null>(null);
@@ -67,6 +84,10 @@ export function XTermLogViewer({
67
84
  const wsRef = useRef<WebSocket | null>(null);
68
85
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
69
86
  const reconnectAttemptsRef = useRef(0);
87
+ const lastSeqRef = useRef<number | null>(null);
88
+ const hasConnectedBeforeRef = useRef(false);
89
+ const serverSupportsReplayRef = useRef(false);
90
+ const skipHistoryOnceRef = useRef(false);
70
91
 
71
92
  const [isConnected, setIsConnected] = useState(false);
72
93
  const [isConnecting, setIsConnecting] = useState(false);
@@ -78,10 +99,47 @@ export function XTermLogViewer({
78
99
 
79
100
  const searchInputRef = useRef<HTMLInputElement>(null);
80
101
  const colors = getAgentColor(agentName);
102
+ const shouldFilterMockOutput = Boolean(suppressNoisyOutput && isMockMode);
81
103
 
82
104
  // Get WebSocket URL from workspace context (handles cloud vs local mode)
83
105
  const logStreamUrl = useWorkspaceWsUrl(`/ws/logs/${encodeURIComponent(agentName)}`);
84
106
 
107
+ const fitTerminal = useCallback(() => {
108
+ const terminal = terminalRef.current;
109
+ const fitAddon = fitAddonRef.current;
110
+ if (!terminal || !fitAddon) {
111
+ return;
112
+ }
113
+
114
+ try {
115
+ fitAddon.fit();
116
+ if (terminal.cols < 80) {
117
+ terminal.resize(80, terminal.rows);
118
+ }
119
+ } catch {
120
+ // Ignore transient layout timing issues.
121
+ }
122
+ }, []);
123
+
124
+ const enqueueLine = useCallback((line: string, appendNewline = false) => {
125
+ const terminal = terminalRef.current;
126
+ if (!terminal) return;
127
+
128
+ const content = appendNewline && !line.endsWith('\n') ? `${line}\n` : line;
129
+
130
+ // Filtering is intentionally disabled for real streams to keep xterm lossless.
131
+ // Preserve prop plumbing so dev/mock harnesses can keep passing the flag.
132
+ if (shouldFilterMockOutput) {
133
+ // no-op: raw stream is rendered directly.
134
+ }
135
+ terminal.write(content);
136
+
137
+ const newlineCount = (content.match(/\n/g) || []).length;
138
+ if (newlineCount > 0) {
139
+ setLineCount((count) => count + newlineCount);
140
+ }
141
+ }, [shouldFilterMockOutput]);
142
+
85
143
  // Initialize terminal
86
144
  useEffect(() => {
87
145
  if (!containerRef.current) return;
@@ -106,7 +164,13 @@ export function XTermLogViewer({
106
164
  terminal.loadAddon(searchAddon);
107
165
 
108
166
  terminal.open(containerRef.current);
109
- fitAddon.fit();
167
+ fitTerminal();
168
+ requestAnimationFrame(() => {
169
+ fitTerminal();
170
+ requestAnimationFrame(() => {
171
+ fitTerminal();
172
+ });
173
+ });
110
174
 
111
175
  terminalRef.current = terminal;
112
176
  fitAddonRef.current = fitAddon;
@@ -114,20 +178,28 @@ export function XTermLogViewer({
114
178
  setIsTerminalReady(true);
115
179
 
116
180
  // Handle resize
181
+ const scheduleFit = () => {
182
+ requestAnimationFrame(() => {
183
+ fitTerminal();
184
+ });
185
+ };
186
+
117
187
  const resizeObserver = new ResizeObserver(() => {
118
- fitAddon.fit();
188
+ scheduleFit();
119
189
  });
120
190
  resizeObserver.observe(containerRef.current);
191
+ window.addEventListener('resize', scheduleFit);
121
192
 
122
193
  return () => {
123
194
  resizeObserver.disconnect();
195
+ window.removeEventListener('resize', scheduleFit);
124
196
  terminal.dispose();
125
197
  terminalRef.current = null;
126
198
  fitAddonRef.current = null;
127
199
  searchAddonRef.current = null;
128
200
  setIsTerminalReady(false);
129
201
  };
130
- }, []);
202
+ }, [fitTerminal]);
131
203
 
132
204
  // Mobile touch scrolling - attach handlers to container, not viewport
133
205
  // xterm.js renders to a canvas which intercepts events; we need to handle
@@ -211,6 +283,8 @@ export function XTermLogViewer({
211
283
 
212
284
  // Connect to WebSocket
213
285
  const connect = useCallback(() => {
286
+ if (isMockMode) return;
287
+
214
288
  if (wsRef.current?.readyState === WebSocket.OPEN ||
215
289
  wsRef.current?.readyState === WebSocket.CONNECTING) {
216
290
  return;
@@ -228,13 +302,29 @@ export function XTermLogViewer({
228
302
  setError(null);
229
303
  reconnectAttemptsRef.current = 0;
230
304
 
231
- terminalRef.current?.writeln(`\x1b[90m[Connected to ${agentName} log stream]\x1b[0m`);
305
+ enqueueLine(`\x1b[90m[Connected to ${agentName} log stream]\x1b[0m`, true);
306
+
307
+ if (
308
+ hasConnectedBeforeRef.current &&
309
+ serverSupportsReplayRef.current &&
310
+ lastSeqRef.current !== null
311
+ ) {
312
+ skipHistoryOnceRef.current = true;
313
+ ws.send(JSON.stringify({
314
+ type: 'replay',
315
+ agent: agentName,
316
+ lastSequenceId: lastSeqRef.current,
317
+ }));
318
+ }
319
+
320
+ hasConnectedBeforeRef.current = true;
232
321
  };
233
322
 
234
323
  ws.onclose = (event) => {
235
324
  setIsConnected(false);
236
325
  setIsConnecting(false);
237
326
  wsRef.current = null;
327
+ skipHistoryOnceRef.current = false;
238
328
 
239
329
  if (reconnectTimeoutRef.current) {
240
330
  clearTimeout(reconnectTimeoutRef.current);
@@ -243,7 +333,7 @@ export function XTermLogViewer({
243
333
 
244
334
  // Don't reconnect for agent not found
245
335
  if (event.code === 4404) {
246
- terminalRef.current?.writeln(`\x1b[31m[Agent not found]\x1b[0m`);
336
+ enqueueLine(`\x1b[31m[Agent not found]\x1b[0m`);
247
337
  return;
248
338
  }
249
339
 
@@ -251,7 +341,7 @@ export function XTermLogViewer({
251
341
  const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000);
252
342
  reconnectAttemptsRef.current++;
253
343
 
254
- terminalRef.current?.writeln(`\x1b[90m[Disconnected. Reconnecting in ${delay / 1000}s...]\x1b[0m`);
344
+ enqueueLine(`\x1b[90m[Disconnected. Reconnecting in ${delay / 1000}s...]\x1b[0m`, true);
255
345
 
256
346
  reconnectTimeoutRef.current = setTimeout(() => {
257
347
  connect();
@@ -261,15 +351,28 @@ export function XTermLogViewer({
261
351
  ws.onerror = () => {
262
352
  setError(new Error('WebSocket connection error'));
263
353
  setIsConnecting(false);
354
+ skipHistoryOnceRef.current = false;
264
355
  };
265
356
 
266
357
  ws.onmessage = (event) => {
267
358
  try {
268
359
  const data = JSON.parse(event.data);
269
360
 
361
+ if (data.type === 'sync') {
362
+ if (typeof data.sequenceId === 'number') {
363
+ serverSupportsReplayRef.current = true;
364
+ if (lastSeqRef.current === null) {
365
+ lastSeqRef.current = data.sequenceId;
366
+ } else {
367
+ lastSeqRef.current = Math.max(lastSeqRef.current, data.sequenceId);
368
+ }
369
+ }
370
+ return;
371
+ }
372
+
270
373
  // Handle different message types
271
374
  if (data.type === 'error') {
272
- terminalRef.current?.writeln(`\x1b[31mError: ${data.error}\x1b[0m`);
375
+ enqueueLine(`\x1b[31mError: ${data.error}\x1b[0m`);
273
376
  return;
274
377
  }
275
378
 
@@ -279,24 +382,46 @@ export function XTermLogViewer({
279
382
 
280
383
  // Handle history (initial log dump)
281
384
  if (data.type === 'history' && Array.isArray(data.lines)) {
385
+ if (skipHistoryOnceRef.current) {
386
+ skipHistoryOnceRef.current = false;
387
+ return;
388
+ }
282
389
  data.lines.forEach((line: string) => {
283
- terminalRef.current?.writeln(line);
284
- setLineCount((c) => c + 1);
390
+ enqueueLine(line, true);
285
391
  });
286
392
  return;
287
393
  }
288
394
 
395
+ if (data.type === 'replay') {
396
+ skipHistoryOnceRef.current = false;
397
+ if (Array.isArray(data.messages)) {
398
+ data.messages.forEach((entry: { seq?: number; content?: string; data?: string; message?: string }) => {
399
+ if (typeof entry.seq === 'number') {
400
+ lastSeqRef.current = Math.max(lastSeqRef.current ?? 0, entry.seq);
401
+ }
402
+ const content = entry.content || entry.data || entry.message || '';
403
+ if (content) {
404
+ enqueueLine(content, false);
405
+ }
406
+ });
407
+ } else if (Array.isArray(data.entries)) {
408
+ data.entries.forEach((entry: { content?: string }) => {
409
+ if (entry.content) {
410
+ enqueueLine(entry.content, false);
411
+ }
412
+ });
413
+ }
414
+ return;
415
+ }
416
+
289
417
  // Handle live output
290
418
  if (data.type === 'log' || data.type === 'output') {
419
+ if (typeof data.seq === 'number') {
420
+ lastSeqRef.current = Math.max(lastSeqRef.current ?? 0, data.seq);
421
+ }
291
422
  const content = data.content || data.data || data.message || '';
292
423
  if (content) {
293
- // Write raw content - xterm.js handles ANSI codes natively
294
- terminalRef.current?.write(content);
295
- // Count newlines for line count
296
- const newlines = (content.match(/\n/g) || []).length;
297
- if (newlines > 0) {
298
- setLineCount((c) => c + newlines);
299
- }
424
+ enqueueLine(content, false);
300
425
  }
301
426
  return;
302
427
  }
@@ -305,22 +430,17 @@ export function XTermLogViewer({
305
430
  if (data.lines && Array.isArray(data.lines)) {
306
431
  data.lines.forEach((line: string | { content: string }) => {
307
432
  const content = typeof line === 'string' ? line : line.content;
308
- terminalRef.current?.writeln(content);
309
- setLineCount((c) => c + 1);
433
+ enqueueLine(content, true);
310
434
  });
311
435
  }
312
436
  } catch {
313
437
  // Plain text message
314
438
  if (typeof event.data === 'string') {
315
- terminalRef.current?.write(event.data);
316
- const newlines = (event.data.match(/\n/g) || []).length;
317
- if (newlines > 0) {
318
- setLineCount((c) => c + newlines);
319
- }
439
+ enqueueLine(event.data, false);
320
440
  }
321
441
  }
322
442
  };
323
- }, [logStreamUrl, agentName]);
443
+ }, [logStreamUrl, agentName, isMockMode, enqueueLine]);
324
444
 
325
445
  // Disconnect from WebSocket
326
446
  const disconnect = useCallback(() => {
@@ -366,11 +486,50 @@ export function XTermLogViewer({
366
486
 
367
487
  // Auto-connect on mount
368
488
  useEffect(() => {
489
+ if (isMockMode) return;
490
+
369
491
  connect();
370
492
  return () => {
371
493
  disconnect();
372
494
  };
373
- }, [connect, disconnect]);
495
+ }, [connect, disconnect, isMockMode]);
496
+
497
+ // Mock mode: write fixture data directly to terminal
498
+ useEffect(() => {
499
+ if (!isMockMode || !isTerminalReady || !terminalRef.current || !mockData) return;
500
+
501
+ const terminal = terminalRef.current;
502
+ terminal.clear();
503
+ setLineCount(0);
504
+ let cancelled = false;
505
+
506
+ (async () => {
507
+ if (!mockStreaming) {
508
+ for (const line of mockData) {
509
+ if (line.content) {
510
+ enqueueLine(line.content, true);
511
+ }
512
+ }
513
+ return;
514
+ }
515
+
516
+ for (const line of mockData) {
517
+ if (cancelled) break;
518
+
519
+ if (line.delay) {
520
+ await new Promise((resolve) => setTimeout(resolve, line.delay));
521
+ }
522
+ if (cancelled) break;
523
+ if (line.content) {
524
+ enqueueLine(line.content, true);
525
+ }
526
+ }
527
+ })();
528
+
529
+ return () => {
530
+ cancelled = true;
531
+ };
532
+ }, [isMockMode, isTerminalReady, mockData, mockStreaming, enqueueLine]);
374
533
 
375
534
  // Keyboard shortcuts
376
535
  useEffect(() => {
@@ -416,10 +575,13 @@ export function XTermLogViewer({
416
575
  <style>{`
417
576
  .xterm-log-viewer .xterm {
418
577
  height: 100%;
578
+ width: 100%;
419
579
  }
420
580
  .xterm-log-viewer .xterm-viewport {
421
581
  height: 100%;
422
582
  max-height: 100%;
583
+ width: 100%;
584
+ max-width: 100%;
423
585
  overscroll-behavior: contain;
424
586
  }
425
587
  /* On touch devices, disable browser touch handling so our JS handler works */
@@ -427,7 +589,8 @@ export function XTermLogViewer({
427
589
  .xterm-log-viewer .xterm,
428
590
  .xterm-log-viewer .xterm-viewport,
429
591
  .xterm-log-viewer .xterm-screen,
430
- .xterm-log-viewer .xterm-screen canvas {
592
+ .xterm-log-viewer .xterm-screen canvas,
593
+ .xterm-log-viewer .xterm-rows {
431
594
  touch-action: none;
432
595
  }
433
596
  }
@@ -560,7 +723,7 @@ export function XTermLogViewer({
560
723
 
561
724
  {/* Terminal container - touch handlers attached via useEffect */}
562
725
  <div
563
- className="flex-1 min-h-0 overflow-hidden"
726
+ className="flex-1 min-h-0 min-w-0 overflow-hidden"
564
727
  style={{ height: maxHeight, maxHeight, minHeight: '200px' }}
565
728
  >
566
729
  <div
@@ -10,17 +10,36 @@
10
10
 
11
11
  import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react';
12
12
  import type { ChannelMessage, ChannelMessageListProps, UnreadState } from './types';
13
+ import type { Reaction } from '../../types';
14
+ import { ReactionChips } from '../ReactionChips';
15
+ import { ThinkingIndicator } from '../ThinkingIndicator';
13
16
  import { formatMessageBody } from '../utils/messageFormatting';
17
+ import { formatRelayReplyCountLabel } from '../../lib/relaycastMessageAdapters';
18
+
19
+ /** Convert channel `Record<string, string[]>` reactions to `Reaction[]` */
20
+ function channelReactionsToArray(reactions?: Record<string, string[]>): Reaction[] {
21
+ if (!reactions) return [];
22
+ return Object.entries(reactions).map(([emoji, agents]) => ({
23
+ emoji,
24
+ count: agents.length,
25
+ agents,
26
+ }));
27
+ }
14
28
 
15
29
  export function ChannelMessageList({
16
30
  messages,
17
31
  unreadState,
18
32
  currentUser,
33
+ currentUserInfo,
34
+ onlineUsers = [],
35
+ agents = [],
36
+ humanUsers = [],
19
37
  isLoadingMore = false,
20
38
  hasMore = false,
21
39
  onLoadMore,
22
40
  onThreadClick,
23
41
  onMemberClick,
42
+ onReaction,
24
43
  }: ChannelMessageListProps) {
25
44
  const containerRef = useRef<HTMLDivElement>(null);
26
45
  const bottomRef = useRef<HTMLDivElement>(null);
@@ -146,6 +165,12 @@ export function ChannelMessageList({
146
165
  isOwn={message.from === currentUser}
147
166
  onThreadClick={onThreadClick}
148
167
  onMemberClick={onMemberClick}
168
+ onReaction={onReaction}
169
+ currentUser={currentUser}
170
+ currentUserInfo={currentUserInfo}
171
+ onlineUsers={onlineUsers}
172
+ agents={agents}
173
+ humanUsers={humanUsers}
149
174
  showAvatar={shouldShowAvatar(dateMessages, index)}
150
175
  />
151
176
  </React.Fragment>
@@ -154,6 +179,26 @@ export function ChannelMessageList({
154
179
  </div>
155
180
  ))}
156
181
 
182
+ {/* Thinking indicators for processing agents */}
183
+ {agents.filter((a) => a.isProcessing).map((agent) => (
184
+ <div key={`thinking-${agent.name}`} className="flex gap-3 py-1 mt-3">
185
+ <div className="w-9 flex-shrink-0">
186
+ <Avatar name={agent.name} avatarUrl={agent.avatarUrl} entityType="agent" />
187
+ </div>
188
+ <div className="flex-1 min-w-0">
189
+ <div className="flex items-center gap-2 mb-0.5">
190
+ <span className="text-sm font-semibold text-text-primary">{agent.name}</span>
191
+ </div>
192
+ <ThinkingIndicator
193
+ isProcessing={true}
194
+ processingStartedAt={agent.processingStartedAt}
195
+ size="small"
196
+ showLabel={true}
197
+ />
198
+ </div>
199
+ </div>
200
+ ))}
201
+
157
202
  {/* Scroll anchor */}
158
203
  <div ref={bottomRef} />
159
204
  </div>
@@ -181,6 +226,24 @@ interface MessageItemProps {
181
226
  isOwn: boolean;
182
227
  onThreadClick?: (messageId: string) => void;
183
228
  onMemberClick?: (memberId: string, entityType: 'user' | 'agent') => void;
229
+ onReaction?: (messageId: string, emoji: string, hasReacted: boolean) => void;
230
+ currentUser?: string;
231
+ currentUserInfo?: {
232
+ displayName: string;
233
+ avatarUrl?: string;
234
+ };
235
+ onlineUsers?: Array<{
236
+ username: string;
237
+ avatarUrl?: string;
238
+ }>;
239
+ agents?: Array<{
240
+ name: string;
241
+ avatarUrl?: string;
242
+ }>;
243
+ humanUsers?: Array<{
244
+ username: string;
245
+ avatarUrl?: string;
246
+ }>;
184
247
  showAvatar: boolean;
185
248
  }
186
249
 
@@ -189,9 +252,24 @@ function MessageItem({
189
252
  isOwn,
190
253
  onThreadClick,
191
254
  onMemberClick,
255
+ onReaction,
256
+ currentUser,
257
+ currentUserInfo,
258
+ onlineUsers = [],
259
+ agents = [],
260
+ humanUsers = [],
192
261
  showAvatar,
193
262
  }: MessageItemProps) {
194
263
  const hasThread = message.threadSummary && message.threadSummary.replyCount > 0;
264
+ const replyCount = message.threadSummary?.replyCount ?? 0;
265
+ const replyLabel = formatRelayReplyCountLabel(replyCount);
266
+ const normalizedSender = message.from.toLowerCase();
267
+
268
+ const avatarUrl = message.fromAvatarUrl
269
+ || (isOwn ? currentUserInfo?.avatarUrl : undefined)
270
+ || onlineUsers.find((user) => user.username.toLowerCase() === normalizedSender)?.avatarUrl
271
+ || humanUsers.find((user) => user.username.toLowerCase() === normalizedSender)?.avatarUrl
272
+ || agents.find((agent) => agent.name.toLowerCase() === normalizedSender)?.avatarUrl;
195
273
 
196
274
  return (
197
275
  <div className={`group relative py-1 ${showAvatar ? 'mt-3' : ''}`}>
@@ -201,8 +279,8 @@ function MessageItem({
201
279
  {showAvatar && (
202
280
  <Avatar
203
281
  name={message.from}
204
- avatarUrl={message.fromAvatarUrl}
205
- entityType={message.fromEntityType}
282
+ avatarUrl={avatarUrl}
283
+ entityType={message.fromEntityType || 'agent'}
206
284
  />
207
285
  )}
208
286
  </div>
@@ -242,11 +320,13 @@ function MessageItem({
242
320
  : 'text-text-muted bg-transparent opacity-0 group-hover:opacity-100 hover:text-accent-cyan hover:bg-accent-cyan/10'}
243
321
  `}
244
322
  onClick={() => onThreadClick?.(message.threadId || message.id)}
245
- title={message.threadId ? `View thread` : (hasThread ? `${message.threadSummary!.replyCount} ${message.threadSummary!.replyCount === 1 ? 'reply' : 'replies'}` : 'Reply in thread')}
323
+ title={message.threadId ? 'View thread' : (hasThread ? replyLabel : 'Reply in thread')}
246
324
  >
247
325
  <ThreadIcon className="w-3.5 h-3.5" />
248
326
  {hasThread && (
249
- <span className="text-xs font-medium">{message.threadSummary!.replyCount}</span>
327
+ <span className="text-xs font-medium">
328
+ {replyLabel}
329
+ </span>
250
330
  )}
251
331
  </button>
252
332
  </div>
@@ -265,6 +345,16 @@ function MessageItem({
265
345
  ))}
266
346
  </div>
267
347
  )}
348
+
349
+ {/* Reactions */}
350
+ {onReaction && (
351
+ <ReactionChips
352
+ reactions={channelReactionsToArray(message.reactions)}
353
+ messageId={message.id}
354
+ currentUser={currentUser}
355
+ onToggleReaction={onReaction}
356
+ />
357
+ )}
268
358
  </div>
269
359
  </div>
270
360
  </div>
@@ -4,7 +4,7 @@
4
4
  * Composed channel view that combines:
5
5
  * - ChannelHeader
6
6
  * - ChannelMessageList
7
- * - MessageInput
7
+ * - MessageComposer
8
8
  *
9
9
  * This is the main view component for displaying a channel's content.
10
10
  */
@@ -12,7 +12,10 @@
12
12
  import React, { useCallback, useMemo } from 'react';
13
13
  import { ChannelHeader } from './ChannelHeader';
14
14
  import { ChannelMessageList } from './ChannelMessageList';
15
- import { MessageInput } from './MessageInput';
15
+ import { MessageComposer } from '../MessageComposer';
16
+ import type { Agent } from '../../types';
17
+ import type { HumanUser } from '../MentionAutocomplete';
18
+ import type { UserPresence } from '../hooks/usePresence';
16
19
  import type {
17
20
  Channel,
18
21
  ChannelMember,
@@ -37,12 +40,21 @@ export interface ChannelViewV1Props {
37
40
  isLoadingMore?: boolean;
38
41
  /** Whether there are more messages to load */
39
42
  hasMoreMessages?: boolean;
40
- /** Available users/agents for @-mentions */
41
- mentionSuggestions?: string[];
43
+ /** Agents available for @-mentions */
44
+ agents?: Agent[];
45
+ /** Human users available for @-mentions */
46
+ humanUsers?: HumanUser[];
47
+ /** Current user profile for avatar fallback */
48
+ currentUserInfo?: {
49
+ displayName: string;
50
+ avatarUrl?: string;
51
+ };
52
+ /** Online users for avatar fallback */
53
+ onlineUsers?: UserPresence[];
42
54
  /** Callback to load more messages */
43
55
  onLoadMore?: () => void;
44
56
  /** Callback to send a message */
45
- onSendMessage: (content: string) => void;
57
+ onSendMessage: (content: string, attachmentIds?: string[]) => Promise<boolean>;
46
58
  /** Callback when editing channel settings */
47
59
  onEditChannel?: () => void;
48
60
  /** Callback to show member list */
@@ -59,6 +71,8 @@ export interface ChannelViewV1Props {
59
71
  onMarkRead?: (upToTimestamp: string) => void;
60
72
  /** Callback when clicking on a member name (for DM navigation) */
61
73
  onMemberClick?: (memberId: string, entityType: 'user' | 'agent') => void;
74
+ /** Callback when toggling a reaction on a message */
75
+ onReaction?: (messageId: string, emoji: string, hasReacted: boolean) => void;
62
76
  }
63
77
 
64
78
  export function ChannelViewV1({
@@ -70,7 +84,10 @@ export function ChannelViewV1({
70
84
  canEditChannel = false,
71
85
  isLoadingMore = false,
72
86
  hasMoreMessages = false,
73
- mentionSuggestions = [],
87
+ agents = [],
88
+ humanUsers = [],
89
+ currentUserInfo,
90
+ onlineUsers = [],
74
91
  onLoadMore,
75
92
  onSendMessage,
76
93
  onEditChannel,
@@ -81,10 +98,11 @@ export function ChannelViewV1({
81
98
  onTyping,
82
99
  onMarkRead,
83
100
  onMemberClick,
101
+ onReaction,
84
102
  }: ChannelViewV1Props) {
85
103
  // Handle send
86
- const handleSend = useCallback((content: string) => {
87
- onSendMessage(content);
104
+ const handleSend = useCallback((content: string, attachmentIds?: string[]) => {
105
+ return onSendMessage(content, attachmentIds);
88
106
  }, [onSendMessage]);
89
107
 
90
108
  // Get placeholder text based on channel type
@@ -116,11 +134,16 @@ export function ChannelViewV1({
116
134
  messages={messages}
117
135
  unreadState={unreadState}
118
136
  currentUser={currentUser}
137
+ currentUserInfo={currentUserInfo}
138
+ onlineUsers={onlineUsers}
139
+ agents={agents}
140
+ humanUsers={humanUsers}
119
141
  isLoadingMore={isLoadingMore}
120
142
  hasMore={hasMoreMessages}
121
143
  onLoadMore={onLoadMore}
122
144
  onThreadClick={onThreadClick}
123
145
  onMemberClick={onMemberClick}
146
+ onReaction={onReaction}
124
147
  />
125
148
 
126
149
  {/* Message Input */}
@@ -131,12 +154,13 @@ export function ChannelViewV1({
131
154
  </p>
132
155
  </div>
133
156
  ) : (
134
- <MessageInput
135
- channelId={channel.id}
157
+ <MessageComposer
136
158
  placeholder={inputPlaceholder}
137
159
  onSend={handleSend}
138
160
  onTyping={onTyping}
139
- mentionSuggestions={mentionSuggestions}
161
+ agents={agents}
162
+ humanUsers={humanUsers}
163
+ enableFileAutocomplete
140
164
  />
141
165
  )}
142
166
  </div>