@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
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for CliToolHarness
3
+ *
4
+ * Covers:
5
+ * - Launching a real tool harness flow (spawn -> log viewer input path)
6
+ * - Releasing tool session
7
+ * - Error handling on launch failure
8
+ */
9
+
10
+ // @vitest-environment jsdom
11
+ import React from 'react';
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
14
+ import { CliToolHarness, type CliToolHarnessConfig } from './CliToolHarness';
15
+ import { api } from '../lib/api';
16
+
17
+ vi.mock('./XTermLogViewer', () => ({
18
+ XTermLogViewer: ({ agentName }: { agentName: string }) => (
19
+ <div data-testid="xterm-log-viewer">{agentName}</div>
20
+ ),
21
+ }));
22
+
23
+ const TOOL: CliToolHarnessConfig = {
24
+ id: 'claude',
25
+ name: 'Claude',
26
+ command: 'claude',
27
+ description: 'Test harness entry',
28
+ };
29
+
30
+ describe('CliToolHarness', () => {
31
+ beforeEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ afterEach(() => {
36
+ cleanup();
37
+ });
38
+
39
+ it('launches a CLI tool and renders a log viewer with the spawned agent', async () => {
40
+ const spawnSpy = vi
41
+ .spyOn(api, 'spawnAgent')
42
+ .mockResolvedValue({ success: true, name: 'claude-tool-1' });
43
+ const releaseSpy = vi
44
+ .spyOn(api, 'releaseAgent')
45
+ .mockResolvedValue({ success: true });
46
+
47
+ render(<CliToolHarness tool={TOOL} nameGenerator={() => 'claude-tool-1'} />);
48
+
49
+ fireEvent.click(screen.getByRole('button', { name: 'Launch Claude' }));
50
+
51
+ await waitFor(() => {
52
+ expect(spawnSpy).toHaveBeenCalledTimes(1);
53
+ expect(spawnSpy).toHaveBeenCalledWith({
54
+ name: 'claude-tool-1',
55
+ cli: 'claude',
56
+ task: undefined,
57
+ });
58
+ expect(screen.getByTestId('xterm-log-viewer')).toHaveTextContent('claude-tool-1');
59
+ expect(screen.getByRole('button', { name: 'Stop Claude' })).toBeTruthy();
60
+ });
61
+
62
+ expect(screen.getByText('Claude')).toBeTruthy();
63
+ fireEvent.click(screen.getByRole('button', { name: 'Stop Claude' }));
64
+
65
+ await waitFor(() => {
66
+ expect(releaseSpy).toHaveBeenCalledTimes(1);
67
+ expect(releaseSpy).toHaveBeenCalledWith('claude-tool-1');
68
+ });
69
+ });
70
+
71
+ it('shows a friendly error message when launch fails', async () => {
72
+ vi
73
+ .spyOn(api, 'spawnAgent')
74
+ .mockResolvedValue({ success: false, name: 'ignored', error: 'Tool unavailable' });
75
+
76
+ render(<CliToolHarness tool={TOOL} />);
77
+
78
+ fireEvent.click(screen.getByRole('button', { name: 'Launch Claude' }));
79
+
80
+ expect(await screen.findByText('Tool unavailable')).toBeTruthy();
81
+ expect(screen.queryByTestId('xterm-log-viewer')).toBeNull();
82
+ });
83
+ });
@@ -0,0 +1,292 @@
1
+ /**
2
+ * CliToolHarness
3
+ *
4
+ * Isolated per-CLI-tool harness used for manual and integration-style testing.
5
+ * Spawns one real CLI tool instance and renders only the CLI metadata + log stream.
6
+ */
7
+
8
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
9
+ import { api } from '../lib/api';
10
+ import { XTermLogViewer } from './XTermLogViewer';
11
+
12
+ export interface CliToolHarnessConfig {
13
+ /** Unique identifier used for CLI and agent name generation */
14
+ id: string;
15
+ /** Display name in the UI */
16
+ name: string;
17
+ /** Command string sent to /api/spawn as `cli` */
18
+ command: string;
19
+ /** Optional task string sent to /api/spawn */
20
+ task?: string;
21
+ /** Optional short description */
22
+ description?: string;
23
+ }
24
+
25
+ export interface CliToolHarnessProps {
26
+ /** Tool definition */
27
+ tool: CliToolHarnessConfig;
28
+ /** Optional class name for the wrapper card */
29
+ className?: string;
30
+ /** Optional deterministic agent-name generator for tests */
31
+ nameGenerator?: (tool: CliToolHarnessConfig) => string;
32
+ }
33
+
34
+ type HarnessState = 'idle' | 'spawning' | 'running' | 'stopping' | 'error';
35
+
36
+ let harnessCounter = 0;
37
+
38
+ const defaultNameGenerator = (tool: CliToolHarnessConfig): string => {
39
+ harnessCounter += 1;
40
+ return `${tool.id}-${Date.now().toString(36)}-${harnessCounter.toString().padStart(3, '0')}`;
41
+ };
42
+
43
+ function getStatusLabel(state: HarnessState): string {
44
+ switch (state) {
45
+ case 'spawning':
46
+ return 'starting';
47
+ case 'stopping':
48
+ return 'stopping';
49
+ case 'running':
50
+ return 'running';
51
+ case 'error':
52
+ return 'error';
53
+ case 'idle':
54
+ default:
55
+ return 'idle';
56
+ }
57
+ }
58
+
59
+ function getButtonLabel(state: HarnessState, toolName: string): string {
60
+ if (state === 'running') {
61
+ return `Stop ${toolName}`;
62
+ }
63
+
64
+ if (state === 'spawning' || state === 'stopping') {
65
+ return `${state === 'spawning' ? 'Starting' : 'Stopping'} ${toolName}...`;
66
+ }
67
+
68
+ return `Launch ${toolName}`;
69
+ }
70
+
71
+ export function CliToolHarness({
72
+ tool,
73
+ className = '',
74
+ nameGenerator = defaultNameGenerator,
75
+ }: CliToolHarnessProps) {
76
+ const [state, setState] = useState<HarnessState>('idle');
77
+ const [error, setError] = useState<string | null>(null);
78
+ const [agentName, setAgentName] = useState<string | null>(null);
79
+ const [chatInput, setChatInput] = useState('');
80
+ const [isSending, setIsSending] = useState(false);
81
+ const [sendError, setSendError] = useState<string | null>(null);
82
+ const activeAgentNameRef = useRef<string | null>(null);
83
+
84
+ useEffect(() => {
85
+ activeAgentNameRef.current = agentName;
86
+ }, [agentName]);
87
+
88
+ useEffect(() => {
89
+ return () => {
90
+ const runningAgentName = activeAgentNameRef.current;
91
+ if (!runningAgentName) return;
92
+ void api.releaseAgent(runningAgentName);
93
+ };
94
+ }, []);
95
+
96
+ const isBusy = state === 'spawning' || state === 'stopping';
97
+
98
+ const handleToggle = useCallback(async () => {
99
+ if (isBusy) return;
100
+
101
+ if (state === 'running' && agentName) {
102
+ setState('stopping');
103
+ setError(null);
104
+
105
+ try {
106
+ const result = await api.releaseAgent(agentName);
107
+ if (!result.success) {
108
+ setState('error');
109
+ setError(result.error || `Failed to stop ${tool.name}`);
110
+ setSendError(null);
111
+ return;
112
+ }
113
+
114
+ setAgentName(null);
115
+ setChatInput('');
116
+ setSendError(null);
117
+ setState('idle');
118
+ } catch {
119
+ setState('error');
120
+ setError(`Failed to stop ${tool.name}`);
121
+ }
122
+ return;
123
+ }
124
+
125
+ setState('spawning');
126
+ setError(null);
127
+
128
+ const name = nameGenerator(tool);
129
+ try {
130
+ const result = await api.spawnAgent({
131
+ name,
132
+ cli: tool.command,
133
+ task: tool.task,
134
+ });
135
+
136
+ if (!result.success) {
137
+ setState('error');
138
+ setError(result.error || `Failed to launch ${tool.name}`);
139
+ return;
140
+ }
141
+
142
+ setAgentName(result.name);
143
+ setSendError(null);
144
+ setState('running');
145
+ } catch {
146
+ setState('error');
147
+ setError(`Failed to launch ${tool.name}`);
148
+ }
149
+ }, [agentName, isBusy, nameGenerator, state, tool]);
150
+
151
+ const handleSendMessage = useCallback(
152
+ async (event: React.FormEvent<HTMLFormElement>) => {
153
+ event.preventDefault();
154
+
155
+ if (!agentName) return;
156
+ const message = chatInput.trim();
157
+ if (!message || isSending) return;
158
+
159
+ setIsSending(true);
160
+ setSendError(null);
161
+
162
+ try {
163
+ const result = await api.sendMessage({
164
+ to: agentName,
165
+ message,
166
+ });
167
+
168
+ if (!result.success) {
169
+ setSendError(result.error || `Failed to send message to ${tool.name}`);
170
+ return;
171
+ }
172
+
173
+ setChatInput('');
174
+ } catch {
175
+ setSendError(`Failed to send message to ${tool.name}`);
176
+ } finally {
177
+ setIsSending(false);
178
+ }
179
+ },
180
+ [agentName, chatInput, isSending, tool.name],
181
+ );
182
+
183
+ const statusLabel = getStatusLabel(state);
184
+ const buttonLabel = getButtonLabel(state, tool.name);
185
+
186
+ return (
187
+ <section
188
+ className={`rounded-xl border border-[#2a2d35] bg-gradient-to-b from-[#0d0f14] to-[#0a0c10] p-4 min-w-0 ${className}`}
189
+ data-tool-id={tool.id}
190
+ >
191
+ <div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
192
+ <div className="min-w-0">
193
+ <h2 className="text-sm font-semibold text-[#e6edf3]">{tool.name}</h2>
194
+ <p className="mt-1 text-xs text-[#8b949e]">Command: {tool.command}</p>
195
+ {tool.description && (
196
+ <p className="mt-2 text-xs text-[#8b949e]">{tool.description}</p>
197
+ )}
198
+ </div>
199
+ <div className="text-xs text-[#8b949e]">
200
+ <span
201
+ className={`rounded-full px-2 py-1 uppercase tracking-wider ${
202
+ state === 'running'
203
+ ? 'bg-[#3fb950]/20 text-[#3fb950]'
204
+ : state === 'error'
205
+ ? 'bg-[#f85149]/20 text-[#f85149]'
206
+ : 'bg-[#30363d]/50 text-[#8b949e]'
207
+ }`}
208
+ >
209
+ {statusLabel}
210
+ </span>
211
+ </div>
212
+ </div>
213
+
214
+ <div className="mb-3 flex flex-wrap items-center gap-2">
215
+ <button
216
+ type="button"
217
+ onClick={handleToggle}
218
+ disabled={isBusy}
219
+ className={`rounded-lg px-3 py-2 text-xs font-semibold transition-all ${
220
+ state === 'running'
221
+ ? 'bg-[#f85149]/20 text-[#fba8a8] hover:bg-[#f85149]/30'
222
+ : 'bg-accent-cyan/20 text-accent-cyan hover:bg-accent-cyan/30'
223
+ }`}
224
+ >
225
+ {buttonLabel}
226
+ </button>
227
+
228
+ {agentName && (
229
+ <span className="text-xs text-[#8b949e]">
230
+ Agent: <span className="text-[#c9d1d9]">{agentName}</span>
231
+ </span>
232
+ )}
233
+ </div>
234
+
235
+ {error && (
236
+ <div
237
+ className="mb-3 rounded-md border border-[#f85149]/40 bg-[#3d1d20] px-3 py-2 text-xs text-[#f85149]"
238
+ role="status"
239
+ aria-live="polite"
240
+ >
241
+ {error}
242
+ </div>
243
+ )}
244
+
245
+ {state === 'running' && agentName ? (
246
+ <div className="mb-3">
247
+ <form className="flex gap-2" onSubmit={handleSendMessage}>
248
+ <input
249
+ type="text"
250
+ value={chatInput}
251
+ onChange={(event) => setChatInput(event.target.value)}
252
+ placeholder={`Message ${agentName}`}
253
+ className="min-w-0 flex-1 rounded-lg border border-[#30363d] bg-[#0d1117] px-2 py-2 text-xs text-[#c9d1d9] placeholder:text-[#6e7681] focus:border-[#58a6ff] focus:outline-none focus:ring-1 focus:ring-[#58a6ff]/40"
254
+ disabled={isSending}
255
+ autoComplete="off"
256
+ />
257
+ <button
258
+ type="submit"
259
+ className="rounded-lg bg-[#58a6ff]/20 px-3 py-2 text-xs font-semibold text-[#79c0ff] transition-all hover:bg-[#58a6ff]/30 disabled:cursor-not-allowed disabled:opacity-50"
260
+ disabled={!chatInput.trim() || isSending}
261
+ >
262
+ {isSending ? 'Sending…' : 'Send'}
263
+ </button>
264
+ </form>
265
+ {sendError && (
266
+ <div className="mt-2 rounded-md border border-[#f85149]/40 bg-[#3d1d20] px-3 py-2 text-xs text-[#f85149]">
267
+ {sendError}
268
+ </div>
269
+ )}
270
+ </div>
271
+ ) : (
272
+ <div className="mb-3 rounded-lg border border-dashed border-[#30363d] px-3 py-2 text-xs text-[#8b949e]">
273
+ Start the session to send a message.
274
+ </div>
275
+ )}
276
+
277
+ {state === 'running' && agentName ? (
278
+ <XTermLogViewer
279
+ agentName={agentName}
280
+ maxHeight="320px"
281
+ showHeader={true}
282
+ key={`log-viewer-${tool.id}-${agentName}`}
283
+ suppressNoisyOutput={false}
284
+ />
285
+ ) : (
286
+ <div className="rounded-lg border border-dashed border-[#30363d] px-3 py-4 text-xs text-[#8b949e]">
287
+ No active session for this tool. Launch it to start a real log stream.
288
+ </div>
289
+ )}
290
+ </section>
291
+ );
292
+ }
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import React, { useState, useEffect } from 'react';
9
- import { getCsrfToken } from '../lib/cloudApi';
9
+ import { useDashboardConfig } from '../adapters';
10
+ import { getCsrfToken } from '../lib/api';
10
11
  import type { Project } from '../types';
11
12
 
12
13
  export interface RepositoryInfo {
@@ -40,7 +41,6 @@ export interface CoordinatorPanelProps {
40
41
  isOpen: boolean;
41
42
  onClose: () => void;
42
43
  projects: Project[];
43
- isCloudMode?: boolean;
44
44
  /** Whether an Architect agent is already running */
45
45
  hasArchitect?: boolean;
46
46
  /** Callback when Architect is spawned */
@@ -51,10 +51,13 @@ export function CoordinatorPanel({
51
51
  isOpen,
52
52
  onClose,
53
53
  projects,
54
- isCloudMode = false,
55
54
  hasArchitect = false,
56
55
  onArchitectSpawned,
57
56
  }: CoordinatorPanelProps) {
57
+ const { features } = useDashboardConfig();
58
+ const hasWorkspaceFeature = features.workspaces;
59
+ const shouldUseWorkspaceCoordinator = hasWorkspaceFeature;
60
+
58
61
  const [projectGroups, setProjectGroups] = useState<ProjectGroup[]>([]);
59
62
  const [ungroupedRepos, setUngroupedRepos] = useState<RepositoryInfo[]>([]);
60
63
  const [isLoading, setIsLoading] = useState(false);
@@ -76,12 +79,16 @@ export function CoordinatorPanel({
76
79
 
77
80
  // Fetch project groups on open
78
81
  useEffect(() => {
79
- if (isOpen && isCloudMode) {
82
+ if (isOpen && shouldUseWorkspaceCoordinator) {
80
83
  fetchProjectGroups();
81
84
  }
82
- }, [isOpen, isCloudMode]);
85
+ }, [isOpen, shouldUseWorkspaceCoordinator]);
83
86
 
84
87
  const fetchProjectGroups = async () => {
88
+ if (!shouldUseWorkspaceCoordinator) {
89
+ return;
90
+ }
91
+
85
92
  setIsLoading(true);
86
93
  setError(null);
87
94
  try {
@@ -341,7 +348,7 @@ export function CoordinatorPanel({
341
348
  };
342
349
 
343
350
  // Local mode: show spawn architect UI
344
- if (!isCloudMode) {
351
+ if (!shouldUseWorkspaceCoordinator) {
345
352
  const isInBridgeMode = projects.length > 1;
346
353
 
347
354
  return (
@@ -9,6 +9,7 @@
9
9
  import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
10
10
  import { useAgentLogs, type LogLine } from './hooks/useAgentLogs';
11
11
  import { getAgentColor } from '../lib/colors';
12
+ import { sanitizeLogContent, isSpinnerFragment } from '../lib/sanitize-logs';
12
13
  import { XTermLogViewer } from './XTermLogViewer';
13
14
 
14
15
  export type LogViewerMode = 'inline' | 'panel';
@@ -59,13 +60,8 @@ export function LogViewer({
59
60
  return logs.filter((log) => {
60
61
  const stripped = sanitizeLogContent(log.content).trim();
61
62
 
62
- // Filter out empty lines
63
63
  if (stripped.length === 0) return false;
64
-
65
- // Filter out likely spinner fragments (single char or very short non-word content)
66
- // Common spinner chars: ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ | - \ / * . etc.
67
- const spinnerPattern = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷◐◓◑◒●○◉◎|\\\/\-*.\u2800-\u28FF]+$/;
68
- if (stripped.length <= 2 && spinnerPattern.test(stripped)) return false;
64
+ if (isSpinnerFragment(stripped)) return false;
69
65
 
70
66
  return true;
71
67
  });
@@ -233,42 +229,6 @@ function ConnectionBadge({
233
229
  );
234
230
  }
235
231
 
236
- /**
237
- * Strip ANSI escape codes (including degraded sequences like "[38;5;216m")
238
- * and control characters so logs render as clean text.
239
- */
240
- function sanitizeLogContent(text: string): string {
241
- if (!text) return '';
242
-
243
- let result = text;
244
-
245
- // Remove OSC sequences (like window title): \x1b]...(\x07|\x1b\\)
246
- result = result.replace(/\x1b\].*?(?:\x07|\x1b\\)/gs, '');
247
-
248
- // Remove DCS (Device Control String) sequences: \x1bP...\x1b\\
249
- result = result.replace(/\x1bP.*?\x1b\\/gs, '');
250
-
251
- // Remove standard ANSI escape sequences (CSI, SGR, etc.)
252
- result = result.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '');
253
-
254
- // Remove single-character escapes
255
- result = result.replace(/\x1b[@-Z\\-_]/g, '');
256
-
257
- // Remove orphaned CSI sequences that lost their escape byte
258
- result = result.replace(/^\[\??\d+[hlKJHfABCDGPXsu]/gm, '');
259
-
260
- // Remove literal SGR sequences that show up without ESC (e.g. "[38;5;216m")
261
- result = result.replace(/\[\d+(?:;\d+)*m/g, '');
262
-
263
- // Remove carriage returns/backspaces and other control chars (except newline/tab)
264
- result = result.replace(/\r/g, '');
265
- result = result.replace(/.\x08/g, '');
266
- result = result.replace(/\x08+/g, '');
267
- result = result.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
268
-
269
- return result;
270
- }
271
-
272
232
  // Icon components
273
233
  function TerminalIcon({ size = 16 }: { size?: number }) {
274
234
  return (