@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.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
- package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
- package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
- package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
- package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
- package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
- package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
- package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
- package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
- package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
- package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
- package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
- package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
- package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
- package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
- package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
- package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
- package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
- package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
- package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
- package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
- package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
- package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
- package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
- package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
- package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
- package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
- package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
- package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
- package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
- package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
- package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
- package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
- package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
- package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
- package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
- package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
- package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
- package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
- package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
- package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
- package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
- package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
- package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
- package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
- package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
- package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
- package/out/about.html +2 -2
- package/out/about.txt +2 -2
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +2 -2
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +2 -2
- package/out/changelog.html +2 -2
- package/out/changelog.txt +2 -2
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +2 -2
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +2 -2
- package/out/contact.html +2 -2
- package/out/contact.txt +2 -2
- package/out/dev/cli-tools.html +1 -0
- package/out/dev/cli-tools.txt +7 -0
- package/out/dev/log-viewer.html +23 -0
- package/out/dev/log-viewer.txt +7 -0
- package/out/docs.html +2 -2
- package/out/docs.txt +2 -2
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +2 -2
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +2 -2
- package/out/privacy.html +2 -2
- package/out/privacy.txt +2 -2
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +2 -2
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +2 -2
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +2 -2
- package/out/providers.html +1 -1
- package/out/providers.txt +2 -2
- package/out/security.html +2 -2
- package/out/security.txt +2 -2
- package/out/signup.html +2 -2
- package/out/signup.txt +2 -2
- package/out/terms.html +2 -2
- package/out/terms.txt +2 -2
- package/package.json +5 -1
- package/src/adapters/DashboardConfigProvider.tsx +56 -0
- package/src/adapters/cloudFetchAdapter.ts +278 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/types.ts +508 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
- package/src/app/app/onboarding/page.tsx +870 -170
- package/src/app/cloud/link/page.tsx +14 -6
- package/src/app/connect-repos/page.tsx +9 -3
- package/src/app/dev/cli-tools/page.tsx +130 -0
- package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
- package/src/app/dev/log-viewer/fixtures.ts +110 -0
- package/src/app/dev/log-viewer/page.tsx +288 -0
- package/src/app/history/page.tsx +28 -12
- package/src/app/page.tsx +1 -1
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
- package/src/components/AgentCard.tsx +4 -4
- package/src/components/AgentLogPreview.tsx +2 -38
- package/src/components/App.tsx +441 -2624
- package/src/components/CliToolHarness.test.tsx +83 -0
- package/src/components/CliToolHarness.tsx +292 -0
- package/src/components/CoordinatorPanel.tsx +13 -6
- package/src/components/LogViewer.tsx +2 -42
- package/src/components/ProviderAuthFlow.tsx +201 -81
- package/src/components/ProvisioningProgress.tsx +1 -1
- package/src/components/ReactionChips.tsx +2 -1
- package/src/components/SpawnModal.test.tsx +51 -18
- package/src/components/SpawnModal.tsx +175 -207
- package/src/components/TerminalProviderSetup.tsx +1 -1
- package/src/components/ThreadPanel.tsx +2 -0
- package/src/components/WorkspaceContext.tsx +7 -19
- package/src/components/XTermLogViewer.tsx +190 -27
- package/src/components/channels/ChannelMessageList.tsx +94 -4
- package/src/components/channels/ChannelViewV1.tsx +35 -11
- package/src/components/channels/api.ts +21 -20
- package/src/components/channels/types.ts +16 -0
- package/src/components/hooks/index.ts +0 -19
- package/src/components/hooks/useMessages.test.ts +80 -0
- package/src/components/hooks/useMessages.ts +13 -4
- package/src/components/hooks/useOrchestrator.ts +1 -1
- package/src/components/hooks/usePresence.ts +45 -6
- package/src/components/hooks/useThread.ts +83 -46
- package/src/components/hooks/useTrajectory.ts +62 -5
- package/src/components/hooks/useWebSocket.test.ts +358 -0
- package/src/components/hooks/useWebSocket.ts +243 -5
- package/src/components/index.ts +2 -14
- package/src/components/layout/Header.tsx +9 -15
- package/src/components/layout/Sidebar.tsx +1 -8
- package/src/components/settings/SettingsPage.tsx +108 -47
- package/src/components/settings/index.ts +0 -3
- package/src/landing/blogData.ts +1 -1
- package/src/lib/agent-merge.test.ts +2 -2
- package/src/lib/api.ts +8 -38
- package/src/lib/identity.test.ts +139 -0
- package/src/lib/identity.ts +48 -0
- package/src/lib/relaycastMessageAdapters.test.ts +182 -0
- package/src/lib/relaycastMessageAdapters.ts +105 -0
- package/src/lib/sanitize-logs.test.ts +227 -0
- package/src/lib/sanitize-logs.ts +202 -0
- package/src/providers/AgentProvider.tsx +799 -0
- package/src/providers/ChannelProvider.tsx +528 -0
- package/src/providers/CloudWorkspaceProvider.tsx +402 -0
- package/src/providers/MessageProvider.tsx +875 -0
- package/src/providers/RelayConfigProvider.tsx +94 -0
- package/src/providers/SendProvider.tsx +497 -0
- package/src/providers/SettingsProvider.tsx +247 -0
- package/src/providers/index.ts +26 -0
- package/src/types/index.ts +10 -10
- package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
- package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
- package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
- package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
- package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
- package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
- package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
- package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
- package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
- package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
- package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
- package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
- package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
- package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
- package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
- package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
- package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
- package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
- package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
- package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
- package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
- package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
- package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
- package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
- package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
- package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
- package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
- package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
- package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
- package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
- package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
- package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
- package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
- package/src/components/BillingResult.tsx +0 -447
- package/src/components/CloudSessionProvider.tsx +0 -130
- package/src/components/SessionExpiredModal.tsx +0 -128
- package/src/components/WorkspaceStatusIndicator.tsx +0 -396
- package/src/components/hooks/useSession.ts +0 -209
- package/src/components/hooks/useWorkspaceMembers.ts +0 -132
- package/src/components/hooks/useWorkspaceStatus.ts +0 -237
- package/src/components/settings/BillingSettingsPanel.tsx +0 -564
- package/src/components/settings/TeamSettingsPanel.tsx +0 -560
- package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
- package/src/lib/cloudApi.ts +0 -893
- /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 {
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
69
|
+
modelOptions,
|
|
52
70
|
}: SettingsPageProps) {
|
|
53
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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 ?? '
|
|
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
|
-
{
|
|
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 ?? '
|
|
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
|
-
{
|
|
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 ?? '
|
|
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
|
-
{
|
|
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 ?? '
|
|
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
|
-
{
|
|
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
|
-
) :
|
|
436
|
-
<
|
|
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="
|
|
446
|
-
description="
|
|
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
|
-
{
|
|
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
|
-
<
|
|
467
|
-
workspaceId={selectedWorkspaceId}
|
|
468
|
-
currentUserId={currentUserId}
|
|
469
|
-
/>
|
|
522
|
+
<TeamPanelSlot />
|
|
470
523
|
</div>
|
|
471
524
|
) : (
|
|
472
525
|
<EmptyState
|
|
473
526
|
icon={<TeamIcon />}
|
|
474
|
-
title="
|
|
475
|
-
description="
|
|
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
|
-
|
|
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';
|
package/src/landing/blogData.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
160
|
-
|
|
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<
|
|
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
|
+
}
|