@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
@@ -11,18 +11,17 @@
11
11
  */
12
12
 
13
13
  import React, { useState, useEffect, useCallback } from 'react';
14
- import { cloudApi, getCsrfToken } from '../../lib/cloudApi';
15
- import { WorkspaceSettingsPanel } from './WorkspaceSettingsPanel';
16
- import { TeamSettingsPanel } from './TeamSettingsPanel';
17
- import { BillingSettingsPanel } from './BillingSettingsPanel';
14
+ import { useDashboardConfig, type DashboardFeatures } from '../../adapters';
18
15
  import type { Settings, CliType } from './types';
19
- import { CLAUDE_MODEL_OPTIONS, CURSOR_MODEL_OPTIONS, CODEX_MODEL_OPTIONS, GEMINI_MODEL_OPTIONS } from '../SpawnModal';
16
+ import { type ModelOption, DEFAULT_MODEL_OPTIONS } from '../SpawnModal';
17
+
18
+ type SettingsTab = 'dashboard' | 'workspace' | 'team' | 'billing';
20
19
 
21
20
  export interface SettingsPageProps {
22
21
  /** Current user ID for team membership checks */
23
22
  currentUserId?: string;
24
23
  /** Initial tab to show */
25
- initialTab?: 'dashboard' | 'workspace' | 'team' | 'billing';
24
+ initialTab?: SettingsTab;
26
25
  /** Callback when settings page is closed */
27
26
  onClose?: () => void;
28
27
  /** Current dashboard settings */
@@ -33,6 +32,13 @@ export interface SettingsPageProps {
33
32
  activeWorkspaceId?: string | null;
34
33
  /** Callback when repos are added/removed in workspace settings */
35
34
  onReposChanged?: () => void;
35
+ /** Model options per agent type — provided by the host app */
36
+ modelOptions?: {
37
+ claude?: ModelOption[];
38
+ cursor?: ModelOption[];
39
+ codex?: ModelOption[];
40
+ gemini?: ModelOption[];
41
+ };
36
42
  }
37
43
 
38
44
  interface WorkspaceSummary {
@@ -41,21 +47,49 @@ interface WorkspaceSummary {
41
47
  status: string;
42
48
  }
43
49
 
50
+ function isTabEnabled(tab: SettingsTab, features: DashboardFeatures): boolean {
51
+ if (tab === 'workspace') return features.workspaces;
52
+ if (tab === 'team') return features.teams;
53
+ if (tab === 'billing') return features.billing;
54
+ return true;
55
+ }
56
+
57
+ function resolveInitialTab(initialTab: SettingsTab, features: DashboardFeatures): SettingsTab {
58
+ return isTabEnabled(initialTab, features) ? initialTab : 'dashboard';
59
+ }
60
+
61
+ const EMPTY_MODEL_OPTIONS: ModelOption[] = [];
62
+
44
63
  export function SettingsPage({
45
- currentUserId,
46
64
  initialTab = 'dashboard',
47
65
  onClose,
48
66
  settings,
49
67
  onUpdateSettings,
50
68
  activeWorkspaceId,
51
- onReposChanged,
69
+ modelOptions,
52
70
  }: SettingsPageProps) {
53
- const [activeTab, setActiveTab] = useState<'dashboard' | 'workspace' | 'team' | 'billing'>(initialTab);
71
+ const config = useDashboardConfig();
72
+ const { features, api, settingsSlots } = config;
73
+
74
+ const claudeModels = modelOptions?.claude ?? DEFAULT_MODEL_OPTIONS.claude ?? EMPTY_MODEL_OPTIONS;
75
+ const cursorModels = modelOptions?.cursor ?? DEFAULT_MODEL_OPTIONS.cursor ?? EMPTY_MODEL_OPTIONS;
76
+ const codexModels = modelOptions?.codex ?? DEFAULT_MODEL_OPTIONS.codex ?? EMPTY_MODEL_OPTIONS;
77
+ const geminiModels = modelOptions?.gemini ?? DEFAULT_MODEL_OPTIONS.gemini ?? EMPTY_MODEL_OPTIONS;
78
+
79
+ const [activeTab, setActiveTab] = useState<SettingsTab>(() =>
80
+ resolveInitialTab(initialTab, features)
81
+ );
54
82
  const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
55
83
  // Initialize with activeWorkspaceId from parent if provided
56
84
  const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(activeWorkspaceId ?? null);
57
85
  const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(true);
58
86
 
87
+ useEffect(() => {
88
+ if (!isTabEnabled(activeTab, features)) {
89
+ setActiveTab('dashboard');
90
+ }
91
+ }, [activeTab, features.billing, features.teams, features.workspaces]);
92
+
59
93
  // Sync selectedWorkspaceId when activeWorkspaceId prop changes
60
94
  useEffect(() => {
61
95
  if (activeWorkspaceId) {
@@ -63,23 +97,49 @@ export function SettingsPage({
63
97
  }
64
98
  }, [activeWorkspaceId]);
65
99
 
66
- // Load workspaces
100
+ // Load workspaces only when workspace features and API adapter are available.
67
101
  useEffect(() => {
102
+ const apiAdapter = api;
103
+
104
+ if (!features.workspaces || !apiAdapter) {
105
+ setIsLoadingWorkspaces(false);
106
+ setWorkspaces([]);
107
+ return;
108
+ }
109
+
110
+ const workspaceApi = apiAdapter;
111
+ let cancelled = false;
112
+
68
113
  async function loadWorkspaces() {
69
114
  setIsLoadingWorkspaces(true);
70
- const result = await cloudApi.getWorkspaceSummary();
115
+ const result = await workspaceApi.getWorkspaceSummary();
116
+
117
+ if (cancelled) {
118
+ return;
119
+ }
120
+
71
121
  if (result.success && result.data.workspaces.length > 0) {
72
- setWorkspaces(result.data.workspaces);
122
+ const summaries = result.data.workspaces.map((workspace) => ({
123
+ id: workspace.id,
124
+ name: workspace.name,
125
+ status: workspace.status,
126
+ }));
127
+ setWorkspaces(summaries);
73
128
  // Only auto-select first workspace if no workspace is selected
74
129
  // (either from prop or previous user selection)
75
- if (!selectedWorkspaceId) {
76
- setSelectedWorkspaceId(result.data.workspaces[0].id);
77
- }
130
+ setSelectedWorkspaceId((prev) => prev ?? summaries[0].id);
131
+ } else {
132
+ setWorkspaces([]);
78
133
  }
79
134
  setIsLoadingWorkspaces(false);
80
135
  }
136
+
81
137
  loadWorkspaces();
82
- }, [selectedWorkspaceId]);
138
+
139
+ return () => {
140
+ cancelled = true;
141
+ };
142
+ }, [api, features.workspaces]);
83
143
 
84
144
  const updateSettings = useCallback((updater: (prev: Settings) => Settings) => {
85
145
  onUpdateSettings(updater);
@@ -98,13 +158,19 @@ export function SettingsPage({
98
158
  });
99
159
  }, [updateSettings]);
100
160
 
101
- const tabs = [
161
+ const allTabs = [
102
162
  { id: 'dashboard', label: 'Dashboard', icon: <DashboardIcon /> },
103
163
  { id: 'workspace', label: 'Workspace', icon: <WorkspaceIcon /> },
104
164
  { id: 'team', label: 'Team', icon: <TeamIcon /> },
105
165
  { id: 'billing', label: 'Billing', icon: <BillingIcon /> },
106
166
  ] as const;
107
167
 
168
+ const tabs = allTabs.filter((tab) => isTabEnabled(tab.id, features));
169
+
170
+ const BillingPanelSlot = settingsSlots?.BillingPanel;
171
+ const TeamPanelSlot = settingsSlots?.TeamPanel;
172
+ const WorkspacePanelSlot = settingsSlots?.WorkspacePanel;
173
+
108
174
  return (
109
175
  <div className="fixed inset-0 z-[1100] bg-bg-deep">
110
176
  {/* Background Pattern */}
@@ -328,7 +394,7 @@ export function SettingsPage({
328
394
  description="Default model when spawning Claude agents"
329
395
  >
330
396
  <select
331
- value={settings.agentDefaults?.defaultModels?.claude ?? 'sonnet'}
397
+ value={settings.agentDefaults?.defaultModels?.claude ?? claudeModels[0]?.value ?? ''}
332
398
  onChange={(e) => updateSettings((prev) => ({
333
399
  ...prev,
334
400
  agentDefaults: {
@@ -341,7 +407,7 @@ export function SettingsPage({
341
407
  }))}
342
408
  className="px-4 py-2 bg-bg-tertiary border border-border-subtle rounded-lg text-sm text-text-primary focus:outline-none focus:border-accent-cyan"
343
409
  >
344
- {CLAUDE_MODEL_OPTIONS.map((model) => (
410
+ {claudeModels.map((model) => (
345
411
  <option key={model.value} value={model.value}>{model.label}</option>
346
412
  ))}
347
413
  </select>
@@ -352,7 +418,7 @@ export function SettingsPage({
352
418
  description="Default model when spawning Cursor agents"
353
419
  >
354
420
  <select
355
- value={settings.agentDefaults?.defaultModels?.cursor ?? 'opus-4.5-thinking'}
421
+ value={settings.agentDefaults?.defaultModels?.cursor ?? cursorModels[0]?.value ?? ''}
356
422
  onChange={(e) => updateSettings((prev) => ({
357
423
  ...prev,
358
424
  agentDefaults: {
@@ -365,7 +431,7 @@ export function SettingsPage({
365
431
  }))}
366
432
  className="px-4 py-2 bg-bg-tertiary border border-border-subtle rounded-lg text-sm text-text-primary focus:outline-none focus:border-accent-cyan"
367
433
  >
368
- {CURSOR_MODEL_OPTIONS.map((model) => (
434
+ {cursorModels.map((model) => (
369
435
  <option key={model.value} value={model.value}>{model.label}</option>
370
436
  ))}
371
437
  </select>
@@ -376,7 +442,7 @@ export function SettingsPage({
376
442
  description="Default model when spawning Codex agents"
377
443
  >
378
444
  <select
379
- value={settings.agentDefaults?.defaultModels?.codex ?? 'gpt-5.2-codex'}
445
+ value={settings.agentDefaults?.defaultModels?.codex ?? codexModels[0]?.value ?? ''}
380
446
  onChange={(e) => updateSettings((prev) => ({
381
447
  ...prev,
382
448
  agentDefaults: {
@@ -389,7 +455,7 @@ export function SettingsPage({
389
455
  }))}
390
456
  className="px-4 py-2 bg-bg-tertiary border border-border-subtle rounded-lg text-sm text-text-primary focus:outline-none focus:border-accent-cyan"
391
457
  >
392
- {CODEX_MODEL_OPTIONS.map((model) => (
458
+ {codexModels.map((model) => (
393
459
  <option key={model.value} value={model.value}>{model.label}</option>
394
460
  ))}
395
461
  </select>
@@ -400,7 +466,7 @@ export function SettingsPage({
400
466
  description="Default model when spawning Gemini agents"
401
467
  >
402
468
  <select
403
- value={settings.agentDefaults?.defaultModels?.gemini ?? 'gemini-2.5-pro'}
469
+ value={settings.agentDefaults?.defaultModels?.gemini ?? geminiModels[0]?.value ?? ''}
404
470
  onChange={(e) => updateSettings((prev) => ({
405
471
  ...prev,
406
472
  agentDefaults: {
@@ -413,7 +479,7 @@ export function SettingsPage({
413
479
  }))}
414
480
  className="px-4 py-2 bg-bg-tertiary border border-border-subtle rounded-lg text-sm text-text-primary focus:outline-none focus:border-accent-cyan"
415
481
  >
416
- {GEMINI_MODEL_OPTIONS.map((model) => (
482
+ {geminiModels.map((model) => (
417
483
  <option key={model.value} value={model.value}>{model.label}</option>
418
484
  ))}
419
485
  </select>
@@ -432,23 +498,13 @@ export function SettingsPage({
432
498
  </div>
433
499
  <span className="ml-4 text-text-muted">Loading workspaces...</span>
434
500
  </div>
435
- ) : selectedWorkspaceId ? (
436
- <WorkspaceSettingsPanel
437
- workspaceId={selectedWorkspaceId}
438
- csrfToken={getCsrfToken() || undefined}
439
- onClose={onClose}
440
- onReposChanged={onReposChanged}
441
- />
501
+ ) : WorkspacePanelSlot ? (
502
+ <WorkspacePanelSlot />
442
503
  ) : (
443
504
  <EmptyState
444
505
  icon={<WorkspaceIcon />}
445
- title="No Workspace"
446
- description="Create a workspace to get started with Agent Relay."
447
- action={
448
- <button className="px-6 py-3 bg-accent-cyan text-bg-deep font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors">
449
- Create Workspace
450
- </button>
451
- }
506
+ title="Workspace Settings Unavailable"
507
+ description="A workspace settings panel has not been provided for this mode."
452
508
  />
453
509
  )}
454
510
  </>
@@ -457,22 +513,19 @@ export function SettingsPage({
457
513
  {/* Team Settings */}
458
514
  {activeTab === 'team' && (
459
515
  <>
460
- {selectedWorkspaceId ? (
516
+ {TeamPanelSlot ? (
461
517
  <div className="space-y-8">
462
518
  <PageHeader
463
519
  title="Team Settings"
464
520
  subtitle="Manage workspace members and permissions"
465
521
  />
466
- <TeamSettingsPanel
467
- workspaceId={selectedWorkspaceId}
468
- currentUserId={currentUserId}
469
- />
522
+ <TeamPanelSlot />
470
523
  </div>
471
524
  ) : (
472
525
  <EmptyState
473
526
  icon={<TeamIcon />}
474
- title="No Workspace Selected"
475
- description="Select a workspace to manage team members."
527
+ title="Team Settings Unavailable"
528
+ description="A team settings panel has not been provided for this mode."
476
529
  />
477
530
  )}
478
531
  </>
@@ -485,7 +538,15 @@ export function SettingsPage({
485
538
  title="Billing & Subscription"
486
539
  subtitle="Manage your plan and payment methods"
487
540
  />
488
- <BillingSettingsPanel />
541
+ {BillingPanelSlot ? (
542
+ <BillingPanelSlot />
543
+ ) : (
544
+ <EmptyState
545
+ icon={<BillingIcon />}
546
+ title="Billing Settings Unavailable"
547
+ description="A billing settings panel has not been provided for this mode."
548
+ />
549
+ )}
489
550
  </div>
490
551
  )}
491
552
  </div>
@@ -5,7 +5,4 @@
5
5
  */
6
6
 
7
7
  export { SettingsPage, type SettingsPageProps } from './SettingsPage';
8
- export { WorkspaceSettingsPanel, type WorkspaceSettingsPanelProps } from './WorkspaceSettingsPanel';
9
- export { TeamSettingsPanel, type TeamSettingsPanelProps } from './TeamSettingsPanel';
10
- export { BillingSettingsPanel, type BillingSettingsPanelProps } from './BillingSettingsPanel';
11
8
  export { defaultSettings, type Settings } from './types';
@@ -269,7 +269,7 @@ The agent will read the setup guide and start a relay session, ready to coordina
269
269
  Use our SDK to enable agent-to-agent communication in your app:
270
270
 
271
271
  \`\`\`bash
272
- # Install agent relay and start the daemon
272
+ # Install agent relay and start the broker
273
273
  npm install -g agent-relay
274
274
  agent-relay up
275
275
 
@@ -19,7 +19,7 @@ describe('mergeAgentsForDashboard', () => {
19
19
  { name: 'Lead', status: 'online' },
20
20
  ];
21
21
  const localAgents: Agent[] = [
22
- { name: 'Lead', status: 'online', isLocal: true, daemonName: 'local-daemon' },
22
+ { name: 'Lead', status: 'online', isLocal: true, brokerName: 'local-broker' },
23
23
  ];
24
24
 
25
25
  const merged = mergeAgentsForDashboard({ agents, localAgents });
@@ -31,7 +31,7 @@ describe('mergeAgentsForDashboard', () => {
31
31
 
32
32
  it('preserves local agents when no cloud agent exists', () => {
33
33
  const localAgents: Agent[] = [
34
- { name: 'Worker', status: 'online', isLocal: true, daemonName: 'local-daemon' },
34
+ { name: 'Worker', status: 'online', isLocal: true, brokerName: 'local-broker' },
35
35
  ];
36
36
 
37
37
  const merged = mergeAgentsForDashboard({ localAgents });
package/src/lib/api.ts CHANGED
@@ -43,13 +43,13 @@ const API_BASE = getApiBaseUrl();
43
43
  // Storage key for workspace ID persistence
44
44
  const WORKSPACE_ID_KEY = 'agentrelay_workspace_id';
45
45
 
46
- // Workspace ID for cloud mode proxying
46
+ // Workspace ID persistence for workspace-scoped APIs
47
47
  let activeWorkspaceId: string | null = null;
48
48
 
49
49
  // CSRF token storage key
50
50
  const CSRF_TOKEN_KEY = 'agentrelay_csrf_token';
51
51
 
52
- // CSRF token for cloud mode requests - persisted in sessionStorage
52
+ // CSRF token for requests - persisted in sessionStorage
53
53
  let csrfToken: string | null = null;
54
54
 
55
55
  // Initialize from sessionStorage if available
@@ -132,42 +132,12 @@ export function initializeWorkspaceId(): string | null {
132
132
  }
133
133
 
134
134
  /**
135
- * Cloud-native API paths that should NOT be proxied through workspace.
136
- * These are routes handled by the cloud server itself, not workspace daemons.
137
- */
138
- const CLOUD_NATIVE_PATHS = [
139
- '/api/daemons/', // Linked daemons API
140
- '/api/workspaces/', // Workspace management
141
- '/api/providers/', // OAuth providers
142
- '/api/auth/', // Authentication
143
- '/api/billing/', // Billing
144
- '/api/usage/', // Usage metrics
145
- '/api/admin/', // Admin endpoints
146
- '/api/onboarding/', // Onboarding
147
- '/api/repos/', // Repository management
148
- '/api/project-groups/', // Coordinators
149
- '/api/github-app/', // GitHub App
150
- '/api/channels', // Channel proxy handled by dashboard server
151
- '/api/bridge', // Bridge data (multi-repo view)
152
- ];
153
-
154
- /**
155
- * Get the API URL, accounting for cloud mode proxying
135
+ * Get the API URL using the configured API base.
156
136
  * @param path - API path like '/api/spawn' or '/api/send'
157
137
  */
158
138
  export function getApiUrl(path: string): string {
159
- if (activeWorkspaceId) {
160
- // Check if this is a cloud-native path that shouldn't be proxied
161
- const isCloudNative = CLOUD_NATIVE_PATHS.some(prefix => path.startsWith(prefix));
162
- if (isCloudNative) {
163
- return `${API_BASE}${path}`;
164
- }
165
- // In cloud mode, proxy through the cloud server
166
- // Strip /api/ prefix since the proxy endpoint adds it back
167
- const proxyPath = path.startsWith('/api/') ? path.substring(5) : path.replace(/^\//, '');
168
- return `/api/workspaces/${activeWorkspaceId}/proxy/${proxyPath}`;
169
- }
170
- return `${API_BASE}${path}`;
139
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
140
+ return `${API_BASE}${normalizedPath}`;
171
141
  }
172
142
 
173
143
  /**
@@ -408,17 +378,17 @@ export const api = {
408
378
  /**
409
379
  * Send a message via the relay
410
380
  */
411
- async sendMessage(request: SendMessageRequest): Promise<ApiResponse<void>> {
381
+ async sendMessage(request: SendMessageRequest): Promise<ApiResponse<{ messageId?: string }>> {
412
382
  try {
413
383
  const response = await apiFetch(getApiUrl('/api/send'), {
414
384
  method: 'POST',
415
385
  body: JSON.stringify(request),
416
386
  });
417
387
 
418
- const result = await response.json() as { success?: boolean; error?: string };
388
+ const result = await response.json() as { success?: boolean; error?: string; messageId?: string };
419
389
 
420
390
  if (response.ok && result.success) {
421
- return { success: true };
391
+ return { success: true, data: { messageId: result.messageId } };
422
392
  }
423
393
 
424
394
  return { success: false, error: result.error || 'Failed to send message' };
@@ -0,0 +1,139 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { isDashboardVariant, getProjectIdentity, normalizeDashboardName } from './identity';
3
+
4
+ function setRelayUsername(value?: string): void {
5
+ const storage = (globalThis as { localStorage?: Storage }).localStorage;
6
+ if (!storage) return;
7
+ if (value) {
8
+ storage.setItem('relay_username', value);
9
+ } else {
10
+ storage.removeItem('relay_username');
11
+ }
12
+ }
13
+
14
+ function installMockLocalStorage(): void {
15
+ if ((globalThis as { localStorage?: Storage }).localStorage) return;
16
+
17
+ const store = new Map<string, string>();
18
+ const mockStorage = {
19
+ getItem: (key: string) => store.get(key) ?? null,
20
+ setItem: (key: string, value: string) => { store.set(key, value); },
21
+ removeItem: (key: string) => { store.delete(key); },
22
+ clear: () => { store.clear(); },
23
+ key: (index: number) => Array.from(store.keys())[index] ?? null,
24
+ get length() { return store.size; },
25
+ } satisfies Storage;
26
+
27
+ vi.stubGlobal('localStorage', mockStorage);
28
+ }
29
+
30
+ describe('isDashboardVariant', () => {
31
+ it('detects "Dashboard"', () => {
32
+ expect(isDashboardVariant('Dashboard')).toBe(true);
33
+ });
34
+
35
+ it('detects "Dashboard-<hex>"', () => {
36
+ expect(isDashboardVariant('Dashboard-5b8c70e5')).toBe(true);
37
+ });
38
+
39
+ it('detects "dashboard-reader"', () => {
40
+ expect(isDashboardVariant('dashboard-reader')).toBe(true);
41
+ });
42
+
43
+ it('detects "human:dashboard"', () => {
44
+ expect(isDashboardVariant('human:dashboard')).toBe(true);
45
+ });
46
+
47
+ it('is case-insensitive', () => {
48
+ expect(isDashboardVariant('DASHBOARD')).toBe(true);
49
+ expect(isDashboardVariant('dashboard')).toBe(true);
50
+ });
51
+
52
+ it('returns false for normal agent names', () => {
53
+ expect(isDashboardVariant('Natty')).toBe(false);
54
+ expect(isDashboardVariant('worker-1')).toBe(false);
55
+ expect(isDashboardVariant('test-broker-new')).toBe(false);
56
+ });
57
+
58
+ it('returns false for empty/whitespace strings', () => {
59
+ expect(isDashboardVariant('')).toBe(false);
60
+ expect(isDashboardVariant(' ')).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe('getProjectIdentity', () => {
65
+ beforeEach(() => {
66
+ installMockLocalStorage();
67
+ setRelayUsername(undefined);
68
+ });
69
+
70
+ afterEach(() => {
71
+ setRelayUsername(undefined);
72
+ });
73
+
74
+ it('returns explicit identity when provided', () => {
75
+ expect(getProjectIdentity('my-project')).toBe('my-project');
76
+ });
77
+
78
+ it('returns localStorage value when no explicit identity', () => {
79
+ setRelayUsername('stored-user');
80
+ expect(getProjectIdentity()).toBe('stored-user');
81
+ });
82
+
83
+ it('returns "Dashboard" as final fallback', () => {
84
+ expect(getProjectIdentity()).toBe('Dashboard');
85
+ });
86
+
87
+ it('explicit identity takes priority over localStorage', () => {
88
+ setRelayUsername('stored-user');
89
+ expect(getProjectIdentity('explicit-user')).toBe('explicit-user');
90
+ });
91
+
92
+ it('trims whitespace from explicit identity', () => {
93
+ expect(getProjectIdentity(' spaced ')).toBe('spaced');
94
+ });
95
+
96
+ it('skips blank explicit identity and falls back', () => {
97
+ setRelayUsername('stored-user');
98
+ expect(getProjectIdentity(' ')).toBe('stored-user');
99
+ });
100
+ });
101
+
102
+ describe('normalizeDashboardName', () => {
103
+ beforeEach(() => {
104
+ installMockLocalStorage();
105
+ setRelayUsername(undefined);
106
+ });
107
+
108
+ afterEach(() => {
109
+ setRelayUsername(undefined);
110
+ });
111
+
112
+ it('maps Dashboard variants to the project identity', () => {
113
+ setRelayUsername('test-broker-new');
114
+ expect(normalizeDashboardName('Dashboard-5b8c70e5')).toBe('test-broker-new');
115
+ expect(normalizeDashboardName('Dashboard')).toBe('test-broker-new');
116
+ expect(normalizeDashboardName('dashboard-reader')).toBe('test-broker-new');
117
+ });
118
+
119
+ it('passes non-Dashboard names through unchanged', () => {
120
+ expect(normalizeDashboardName('Natty')).toBe('Natty');
121
+ expect(normalizeDashboardName('worker-1')).toBe('worker-1');
122
+ });
123
+
124
+ it('falls back to "Dashboard" when no identity is stored', () => {
125
+ expect(normalizeDashboardName('Dashboard-5b8c70e5')).toBe('Dashboard');
126
+ });
127
+
128
+ it('uses explicit project identity when provided', () => {
129
+ expect(normalizeDashboardName('Dashboard-5b8c70e5', 'my-project')).toBe('my-project');
130
+ });
131
+
132
+ it('returns original value for empty input', () => {
133
+ expect(normalizeDashboardName('')).toBe('');
134
+ });
135
+
136
+ it('trims non-Dashboard names', () => {
137
+ expect(normalizeDashboardName(' Natty ')).toBe('Natty');
138
+ });
139
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Frontend identity resolution module.
3
+ * Single source of truth for Dashboard identity normalization on the client.
4
+ */
5
+
6
+ // Dashboard variant pattern (Dashboard, Dashboard-<hex>, dashboard-reader)
7
+ const DASHBOARD_VARIANT_PATTERN = /^dashboard(-[0-9a-f]{6,}|-reader)?$/i;
8
+
9
+ export function isDashboardVariant(name: string): boolean {
10
+ if (!name?.trim()) return false;
11
+ const lower = name.trim().toLowerCase();
12
+ return lower === 'dashboard' || lower === 'human:dashboard' || DASHBOARD_VARIANT_PATTERN.test(name.trim());
13
+ }
14
+
15
+ /**
16
+ * Get the project display identity from available sources.
17
+ * Priority: explicit param > localStorage > "Dashboard"
18
+ */
19
+ export function getProjectIdentity(explicitIdentity?: string | null): string {
20
+ if (explicitIdentity?.trim()) return explicitIdentity.trim();
21
+ // Try localStorage as fallback (for when context isn't available).
22
+ // Use globalThis to support both browser (window.localStorage) and test environments
23
+ // where vitest stubs localStorage on globalThis without defining window.
24
+ const storage = (
25
+ typeof window !== 'undefined'
26
+ ? window.localStorage
27
+ : (globalThis as { localStorage?: Storage }).localStorage
28
+ );
29
+ if (storage) {
30
+ try {
31
+ const stored = storage.getItem('relay_username');
32
+ if (stored?.trim()) return stored.trim();
33
+ } catch { /* SSR or blocked */ }
34
+ }
35
+ return 'Dashboard';
36
+ }
37
+
38
+ /**
39
+ * Normalize a name: map Dashboard variants to the project identity.
40
+ * Non-Dashboard names pass through unchanged.
41
+ */
42
+ export function normalizeDashboardName(name: string, projectIdentity?: string | null): string {
43
+ if (!name?.trim()) return name;
44
+ if (isDashboardVariant(name.trim())) {
45
+ return getProjectIdentity(projectIdentity);
46
+ }
47
+ return name.trim();
48
+ }