diff --git a/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.test.tsx b/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.test.tsx index 5dfd13e19..72f008eb3 100644 --- a/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.test.tsx +++ b/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.test.tsx @@ -625,7 +625,9 @@ public sealed class DraftBehavior : ScriptBehavior { expect(screen.getByText('Bind saved script')).toBeTruthy(); diff --git a/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.tsx b/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.tsx index 33f14eb75..b95da59b5 100644 --- a/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.tsx +++ b/apps/aevatar-console-web/src/modules/studio/scripts/ScriptsWorkbenchPage.tsx @@ -1501,12 +1501,12 @@ const ScriptsWorkbenchPage: React.FC = ({ await queryClient.invalidateQueries({ queryKey: ['studio-scripts-catalogs', appContext.scopeId], }); - const bindingScopeIds = Array.from( + const defaultRouteScopeIds = Array.from( new Set([appContext.scopeId, resolvedScopeId].filter(Boolean)), ); - for (const scopeId of bindingScopeIds) { + for (const scopeId of defaultRouteScopeIds) { await queryClient.invalidateQueries({ - queryKey: ['studio-scope-binding', scopeId], + queryKey: ['studio-default-route', scopeId], }); } }, [appContext.scopeId, queryClient, resolvedScopeId]); @@ -2672,7 +2672,7 @@ const ScriptsWorkbenchPage: React.FC = ({ className="console-scripts-solid-action console-scripts-header-text-action" onClick={handleOpenBindScope} disabled={!canBindScope} - aria-label="Bind scope" + aria-label="Update default route" > Bind diff --git a/apps/aevatar-console-web/src/pages/chat/chatAdvancedConsole.tsx b/apps/aevatar-console-web/src/pages/chat/chatAdvancedConsole.tsx index c143da909..8d59c9c53 100644 --- a/apps/aevatar-console-web/src/pages/chat/chatAdvancedConsole.tsx +++ b/apps/aevatar-console-web/src/pages/chat/chatAdvancedConsole.tsx @@ -124,7 +124,7 @@ const consoleFlows: readonly ConsoleFlow[] = [ { badge: "Recommended first", description: - "Check the scope binding, published services, deployed workflows, or inspect an actor directly.", + "Check the default route target, published services, deployed workflows, or inspect an actor directly.", group: "understand", id: "query", label: "Query", @@ -702,7 +702,7 @@ export function ChatAdvancedConsole({ let result: unknown; switch (queryTarget) { case "binding": - result = await studioApi.getScopeBinding(scopeId); + result = await studioApi.getDefaultRouteTarget(scopeId); break; case "services": result = await servicesApi.listServices({ diff --git a/apps/aevatar-console-web/src/pages/chat/index.test.tsx b/apps/aevatar-console-web/src/pages/chat/index.test.tsx index c51f7f6e0..478d238c2 100644 --- a/apps/aevatar-console-web/src/pages/chat/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/chat/index.test.tsx @@ -84,6 +84,11 @@ jest.mock("@/shared/studio/api", () => ({ scopeId: "scope-a", serviceId: "support-service", })), + getDefaultRouteTarget: jest.fn(async () => ({ + available: true, + scopeId: "scope-a", + serviceId: "support-service", + })), getUserConfig: jest.fn(async () => ({ defaultModel: "", preferredLlmRoute: "", diff --git a/apps/aevatar-console-web/src/pages/chat/index.tsx b/apps/aevatar-console-web/src/pages/chat/index.tsx index 5cb68da69..4941aa8bc 100644 --- a/apps/aevatar-console-web/src/pages/chat/index.tsx +++ b/apps/aevatar-console-web/src/pages/chat/index.tsx @@ -386,10 +386,10 @@ const ChatPage: React.FC = () => { queryFn: () => studioApi.getUserConfigModels(), }); - const bindingQuery = useQuery({ + const defaultRouteTargetQuery = useQuery({ enabled: scopeId.length > 0, - queryKey: ["chat", "binding", scopeId], - queryFn: () => studioApi.getScopeBinding(scopeId), + queryKey: ["chat", "default-route-target", scopeId], + queryFn: () => studioApi.getDefaultRouteTarget(scopeId), }); const servicesQuery = useQuery({ enabled: scopeId.length > 0, @@ -407,13 +407,19 @@ const ChatPage: React.FC = () => { createOnboardingServiceOption(), ...buildScopeConsoleServiceOptions( servicesQuery.data ?? [], - bindingQuery.data?.available ? bindingQuery.data.serviceId : undefined, + defaultRouteTargetQuery.data?.available + ? defaultRouteTargetQuery.data.serviceId + : undefined, { chatOnly: true, } ).map(mapChatServiceOption), ], - [bindingQuery.data?.available, bindingQuery.data?.serviceId, servicesQuery.data] + [ + defaultRouteTargetQuery.data?.available, + defaultRouteTargetQuery.data?.serviceId, + servicesQuery.data, + ] ); const providerConfigured = useMemo( () => hasConfiguredProviders(settingsQuery.data?.providers ?? []), @@ -575,14 +581,16 @@ const ChatPage: React.FC = () => { const onboardingPreferredServiceId = settingsQuery.isSuccess && !providerConfigured && - !bindingQuery.data?.available && + !defaultRouteTargetQuery.data?.available && services.some((service) => service.id === onboardingServiceId) ? onboardingServiceId : ""; const preferredServiceId = routePreferredServiceId || onboardingPreferredServiceId || - (bindingQuery.data?.available ? bindingQuery.data.serviceId : "") || + (defaultRouteTargetQuery.data?.available + ? defaultRouteTargetQuery.data.serviceId + : "") || services.find((service) => service.id === nyxIdChatServiceId)?.id || services[0]?.id || ""; @@ -609,8 +617,8 @@ const ChatPage: React.FC = () => { } }, [ activeConversationId, - bindingQuery.data?.available, - bindingQuery.data?.serviceId, + defaultRouteTargetQuery.data?.available, + defaultRouteTargetQuery.data?.serviceId, isStreaming, messages.length, providerConfigured, @@ -2197,7 +2205,7 @@ const ChatPage: React.FC = () => { "Open Tools only when you need audit evidence or protocol-level detail.", ] : [ - "Ask NyxID to inspect services, credentials, or scope bindings.", + "Ask NyxID to inspect services, credentials, or default route targets.", "Use natural-language prompts first, then open Tools for deeper runtime evidence.", "Keep model and route overrides in the composer footer when you need a specific provider path.", ] diff --git a/apps/aevatar-console-web/src/pages/gagents/index.test.tsx b/apps/aevatar-console-web/src/pages/gagents/index.test.tsx index 433d97458..2535aa73b 100644 --- a/apps/aevatar-console-web/src/pages/gagents/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/gagents/index.test.tsx @@ -34,9 +34,12 @@ jest.mock("@/shared/api/runtimeGAgentApi", () => ({ listTypes: jest.fn(), listActors: jest.fn(), getScopeBinding: jest.fn(), + getDefaultRouteTarget: jest.fn(), bindScopeGAgent: jest.fn(), activateScopeBindingRevision: jest.fn(), + activateMemberBindingRevision: jest.fn(), retireScopeBindingRevision: jest.fn(), + retireMemberBindingRevision: jest.fn(), addActor: jest.fn(), removeActor: jest.fn(), streamDraftRun: jest.fn(), @@ -106,9 +109,12 @@ describe("GAgentsPage", () => { listTypes: jest.Mock; listActors: jest.Mock; getScopeBinding: jest.Mock; + getDefaultRouteTarget: jest.Mock; bindScopeGAgent: jest.Mock; activateScopeBindingRevision: jest.Mock; + activateMemberBindingRevision: jest.Mock; retireScopeBindingRevision: jest.Mock; + retireMemberBindingRevision: jest.Mock; addActor: jest.Mock; removeActor: jest.Mock; streamDraftRun: jest.Mock; @@ -154,7 +160,7 @@ describe("GAgentsPage", () => { actorIds: ["planner-1"], }, ]; - mockedRuntimeGAgentApi.getScopeBinding.mockResolvedValue({ + mockedRuntimeGAgentApi.getDefaultRouteTarget.mockResolvedValue({ available: false, scopeId: "scope-a", serviceId: "", @@ -181,13 +187,13 @@ describe("GAgentsPage", () => { preferredActorId: "orders-1", }, }); - mockedRuntimeGAgentApi.activateScopeBindingRevision.mockResolvedValue({ + mockedRuntimeGAgentApi.activateMemberBindingRevision.mockResolvedValue({ scopeId: "scope-a", serviceId: "service-orders", displayName: "Orders Assistant", revisionId: "rev-2", }); - mockedRuntimeGAgentApi.retireScopeBindingRevision.mockResolvedValue({ + mockedRuntimeGAgentApi.retireMemberBindingRevision.mockResolvedValue({ scopeId: "scope-a", serviceId: "service-orders", revisionId: "rev-2", @@ -400,7 +406,7 @@ describe("GAgentsPage", () => { }); it("surfaces the current binding and active binding type in the workbench", async () => { - mockedRuntimeGAgentApi.getScopeBinding.mockResolvedValue({ + mockedRuntimeGAgentApi.getDefaultRouteTarget.mockResolvedValue({ available: true, scopeId: "scope-a", serviceId: "service-orders", @@ -453,7 +459,7 @@ describe("GAgentsPage", () => { }); it("requires acknowledgement before replacing a published binding and then publishes the revision", async () => { - mockedRuntimeGAgentApi.getScopeBinding.mockResolvedValue({ + mockedRuntimeGAgentApi.getDefaultRouteTarget.mockResolvedValue({ available: true, scopeId: "scope-a", serviceId: "service-orders", @@ -549,7 +555,7 @@ describe("GAgentsPage", () => { }); it("activates and retires a selectable binding revision", async () => { - mockedRuntimeGAgentApi.getScopeBinding.mockResolvedValue({ + mockedRuntimeGAgentApi.getDefaultRouteTarget.mockResolvedValue({ available: true, scopeId: "scope-a", serviceId: "service-orders", @@ -625,7 +631,7 @@ describe("GAgentsPage", () => { fireEvent.click(await screen.findByRole("button", { name: "Activate" })); await waitFor(() => { expect( - mockedRuntimeGAgentApi.activateScopeBindingRevision + mockedRuntimeGAgentApi.activateMemberBindingRevision ).toHaveBeenCalledWith("scope-a", "rev-2"); }); expect( @@ -640,7 +646,7 @@ describe("GAgentsPage", () => { await waitFor(() => { expect( - mockedRuntimeGAgentApi.retireScopeBindingRevision + mockedRuntimeGAgentApi.retireMemberBindingRevision ).toHaveBeenCalledWith("scope-a", "rev-2"); }); expect( diff --git a/apps/aevatar-console-web/src/pages/gagents/index.tsx b/apps/aevatar-console-web/src/pages/gagents/index.tsx index 764951064..ad62ef585 100644 --- a/apps/aevatar-console-web/src/pages/gagents/index.tsx +++ b/apps/aevatar-console-web/src/pages/gagents/index.tsx @@ -453,7 +453,7 @@ const GAgentsPage: React.FC = () => { const bindingQuery = useQuery({ enabled: normalizedScopeId.length > 0, queryKey: ['runtime-gagents', 'binding', normalizedScopeId], - queryFn: () => runtimeGAgentApi.getScopeBinding(normalizedScopeId), + queryFn: () => runtimeGAgentApi.getDefaultRouteTarget(normalizedScopeId), retry: false, }); @@ -1119,7 +1119,7 @@ const GAgentsPage: React.FC = () => { setBindingPendingKey(`activate:${revisionId}`); setBindingNotice(null); try { - const result = await runtimeGAgentApi.activateScopeBindingRevision( + const result = await runtimeGAgentApi.activateMemberBindingRevision( normalizedScopeId, revisionId, ); @@ -1149,7 +1149,7 @@ const GAgentsPage: React.FC = () => { setBindingPendingKey(`retire:${revisionId}`); setBindingNotice(null); try { - const result = await runtimeGAgentApi.retireScopeBindingRevision( + const result = await runtimeGAgentApi.retireMemberBindingRevision( normalizedScopeId, revisionId, ); diff --git a/apps/aevatar-console-web/src/pages/scopes/assets.test.tsx b/apps/aevatar-console-web/src/pages/scopes/assets.test.tsx index cbd21a23f..f3adc8358 100644 --- a/apps/aevatar-console-web/src/pages/scopes/assets.test.tsx +++ b/apps/aevatar-console-web/src/pages/scopes/assets.test.tsx @@ -103,6 +103,18 @@ jest.mock('@/shared/studio/api', () => ({ : 'actor://scope-a/default', revisions: [], })), + getDefaultRouteTarget: jest.fn(async (scopeId: string) => ({ + available: true, + scopeId: scopeId?.trim() || 'scope-a', + serviceId: scopeId?.trim() === 'scope-b' ? 'ops' : 'default', + displayName: scopeId?.trim() === 'scope-b' ? 'Workspace Beta' : 'Workspace Demo', + serviceKey: scopeId?.trim() === 'scope-b' ? 'scope-b:ops' : 'scope-a:default', + primaryActorId: + scopeId?.trim() === 'scope-b' + ? 'actor://scope-b/ops' + : 'actor://scope-a/default', + revisions: [], + })), }, })); diff --git a/apps/aevatar-console-web/src/pages/scopes/assets.tsx b/apps/aevatar-console-web/src/pages/scopes/assets.tsx index a6848a7b7..cfece2a5c 100644 --- a/apps/aevatar-console-web/src/pages/scopes/assets.tsx +++ b/apps/aevatar-console-web/src/pages/scopes/assets.tsx @@ -37,10 +37,10 @@ import type { } from "@/shared/models/scopes"; import { studioApi } from "@/shared/studio/api"; import { - describeStudioScopeBindingRevisionContext, - describeStudioScopeBindingRevisionTarget, - formatStudioScopeBindingImplementationKind, - getStudioScopeBindingCurrentRevision, + describeStudioDefaultRouteTargetRevisionContext, + describeStudioDefaultRouteTargetRevisionTarget, + formatStudioMemberBindingImplementationKind, + getStudioDefaultRouteTargetCurrentRevision, } from "@/shared/studio/models"; import { buildStudioScriptsWorkspaceRoute, @@ -369,10 +369,10 @@ const TeamAssetsPage: React.FC = () => { queryFn: () => scopesApi.listScripts(activeDraft.scopeId), queryKey: ["scopes", "scripts", activeDraft.scopeId], }); - const bindingQuery = useQuery({ + const defaultRouteQuery = useQuery({ enabled: activeDraft.scopeId.trim().length > 0, - queryFn: () => studioApi.getScopeBinding(activeDraft.scopeId), - queryKey: ["scopes", "binding", activeDraft.scopeId], + queryFn: () => studioApi.getDefaultRouteTarget(activeDraft.scopeId), + queryKey: ["scopes", "default-route", activeDraft.scopeId], }); const workflowDetailQuery = useQuery({ enabled: @@ -423,27 +423,27 @@ const TeamAssetsPage: React.FC = () => { (item) => item.capabilityStatus === "active", ).length; const draftCapabilityCount = workflowCount + scriptCount - activeCapabilityCount; - const currentBindingRevision = getStudioScopeBindingCurrentRevision( - bindingQuery.data, + const currentDefaultRouteRevision = getStudioDefaultRouteTargetCurrentRevision( + defaultRouteQuery.data, ); - const currentBindingLabel = bindingQuery.data?.available - ? currentBindingRevision - ? describeStudioScopeBindingRevisionTarget(currentBindingRevision) - : bindingQuery.data.displayName || bindingQuery.data.serviceId + const currentDefaultRouteLabel = defaultRouteQuery.data?.available + ? currentDefaultRouteRevision + ? describeStudioDefaultRouteTargetRevisionTarget(currentDefaultRouteRevision) + : defaultRouteQuery.data.displayName || defaultRouteQuery.data.serviceId : "Not bound"; - const currentBindingKind = currentBindingRevision - ? formatStudioScopeBindingImplementationKind( - currentBindingRevision.implementationKind, + const currentDefaultRouteKind = currentDefaultRouteRevision + ? formatStudioMemberBindingImplementationKind( + currentDefaultRouteRevision.implementationKind, ) - : bindingQuery.data?.available + : defaultRouteQuery.data?.available ? "Published" : "Unknown"; - const currentBindingContext = describeStudioScopeBindingRevisionContext( - currentBindingRevision, + const currentDefaultRouteContext = describeStudioDefaultRouteTargetRevisionContext( + currentDefaultRouteRevision, ); - const currentBindingActor = - currentBindingRevision?.primaryActorId || - bindingQuery.data?.primaryActorId || + const currentDefaultRouteActor = + currentDefaultRouteRevision?.primaryActorId || + defaultRouteQuery.data?.primaryActorId || ""; const selectedWorkflow = useMemo( @@ -740,8 +740,9 @@ const TeamAssetsPage: React.FC = () => { history.push( buildRuntimeGAgentsHref({ scopeId: activeDraft.scopeId.trim(), - actorId: currentBindingRevision?.primaryActorId || undefined, - actorTypeName: currentBindingRevision?.staticActorTypeName || undefined, + actorId: currentDefaultRouteRevision?.primaryActorId || undefined, + actorTypeName: + currentDefaultRouteRevision?.staticActorTypeName || undefined, }), ) } @@ -830,8 +831,14 @@ const TeamAssetsPage: React.FC = () => { }} > - - + + { value="Stage capability posture first. Open the inspector only when you need source, schema, or catalog detail." /> @@ -609,8 +609,8 @@ const ScopeServiceRuntimeWorkbench: React.FC {revision.retiredAt ? : null} - {describeStudioScopeBindingRevisionTarget(revision)} ·{" "} - {describeStudioScopeBindingRevisionContext(revision) || "No detail"} + {describeStudioMemberBindingRevisionTarget(revision)} ·{" "} + {describeStudioMemberBindingRevisionContext(revision) || "No detail"} Serving {revision.servingState || revision.status} · Published{" "} @@ -856,12 +856,12 @@ const ScopeServiceRuntimeWorkbench: React.FC title={ bindingsQuery.error instanceof Error ? bindingsQuery.error.message - : "Failed to load scope bindings." + : "Failed to load default route revisions." } type="error" /> ) : bindingsQuery.isLoading ? ( - + ) : ( bindingCards )} @@ -915,22 +915,22 @@ const ScopeServiceRuntimeWorkbench: React.FC /> - {describeStudioScopeBindingRevisionContext(currentRevision) ? ( + {describeStudioMemberBindingRevisionContext(currentRevision) ? ( title={ bindingEditorState?.mode === "edit" ? `Edit binding ${bindingEditorState.bindingId || ""}` - : "Create scope binding" + : "Create default route" } > diff --git a/apps/aevatar-console-web/src/pages/scopes/invoke.test.tsx b/apps/aevatar-console-web/src/pages/scopes/invoke.test.tsx index 88bd907ef..36a5bca55 100644 --- a/apps/aevatar-console-web/src/pages/scopes/invoke.test.tsx +++ b/apps/aevatar-console-web/src/pages/scopes/invoke.test.tsx @@ -121,6 +121,20 @@ jest.mock('@/shared/studio/api', () => ({ updatedAt: '2026-03-26T08:00:00Z', revisions: [], })), + getDefaultRouteTarget: jest.fn(async () => ({ + available: true, + scopeId: 'scope-a', + serviceId: 'default', + displayName: 'Workspace Demo', + serviceKey: 'scope-a:default:default:default', + defaultServingRevisionId: 'rev-2', + activeServingRevisionId: 'rev-2', + deploymentId: 'deploy-2', + deploymentStatus: 'Active', + primaryActorId: 'actor://scope-a/default', + updatedAt: '2026-03-26T08:00:00Z', + revisions: [], + })), }, })); diff --git a/apps/aevatar-console-web/src/pages/scopes/invoke.tsx b/apps/aevatar-console-web/src/pages/scopes/invoke.tsx index 91933c89f..6fffeafd2 100644 --- a/apps/aevatar-console-web/src/pages/scopes/invoke.tsx +++ b/apps/aevatar-console-web/src/pages/scopes/invoke.tsx @@ -57,9 +57,9 @@ import { import { studioApi } from '@/shared/studio/api'; import type { ServiceCatalogSnapshot } from '@/shared/models/services'; import { - describeStudioScopeBindingRevisionContext, - describeStudioScopeBindingRevisionTarget, - getStudioScopeBindingCurrentRevision, + describeStudioDefaultRouteTargetRevisionContext, + describeStudioDefaultRouteTargetRevisionTarget, + getStudioDefaultRouteTargetCurrentRevision, } from '@/shared/studio/models'; import { buildStudioWorkflowWorkspaceRoute } from '@/shared/studio/navigation'; import { @@ -527,10 +527,10 @@ const ScopeInvokePage: React.FC = () => { }, [chatMessages]); const scopeId = activeDraft.scopeId.trim(); - const bindingQuery = useQuery({ + const defaultRouteQuery = useQuery({ enabled: scopeId.length > 0, - queryKey: ['scopes', 'binding', scopeId], - queryFn: () => studioApi.getScopeBinding(scopeId), + queryKey: ['scopes', 'default-route', scopeId], + queryFn: () => studioApi.getDefaultRouteTarget(scopeId), }); const scopeServicesQuery = useQuery({ enabled: scopeId.length > 0, @@ -551,11 +551,13 @@ const ScopeInvokePage: React.FC = () => { () => buildPublishedServiceCatalog( scopeServicesQuery.data ?? [], - bindingQuery.data?.available ? bindingQuery.data.serviceId : undefined, + defaultRouteQuery.data?.available + ? defaultRouteQuery.data.serviceId + : undefined, ), [ - bindingQuery.data?.available, - bindingQuery.data?.serviceId, + defaultRouteQuery.data?.available, + defaultRouteQuery.data?.serviceId, scopeServicesQuery.data, ], ); @@ -564,8 +566,8 @@ const ScopeInvokePage: React.FC = () => { scopeId ? buildScopeConsoleServiceOptions( publishedServices, - bindingQuery.data?.available - ? bindingQuery.data.serviceId + defaultRouteQuery.data?.available + ? defaultRouteQuery.data.serviceId : undefined, { sortBy: 'serviceId', @@ -573,8 +575,8 @@ const ScopeInvokePage: React.FC = () => { ) : [], [ - bindingQuery.data?.available, - bindingQuery.data?.serviceId, + defaultRouteQuery.data?.available, + defaultRouteQuery.data?.serviceId, publishedServices, scopeId, ], @@ -597,7 +599,9 @@ const ScopeInvokePage: React.FC = () => { const preferredServiceId = getPreferredScopeConsoleServiceId( services, - bindingQuery.data?.available ? bindingQuery.data.serviceId : undefined, + defaultRouteQuery.data?.available + ? defaultRouteQuery.data.serviceId + : undefined, ); const hasSelectedService = selectedServiceId && @@ -617,7 +621,7 @@ const ScopeInvokePage: React.FC = () => { setSelectedServiceId(preferredServiceId); } }, [ - bindingQuery.data?.serviceId, + defaultRouteQuery.data?.serviceId, preserveEmptySelection, selectedServiceId, services, @@ -657,18 +661,18 @@ const ScopeInvokePage: React.FC = () => { const isChatPlayground = Boolean( selectedEndpoint && isChatServiceEndpoint(selectedEndpoint), ); - const currentBindingRevision = getStudioScopeBindingCurrentRevision( - bindingQuery.data, + const currentDefaultRouteRevision = getStudioDefaultRouteTargetCurrentRevision( + defaultRouteQuery.data, ); - const currentBindingTarget = describeStudioScopeBindingRevisionTarget( - currentBindingRevision, + const currentDefaultRouteTarget = describeStudioDefaultRouteTargetRevisionTarget( + currentDefaultRouteRevision, ); - const currentBindingContext = describeStudioScopeBindingRevisionContext( - currentBindingRevision, + const currentDefaultRouteContext = describeStudioDefaultRouteTargetRevisionContext( + currentDefaultRouteRevision, ); - const currentBindingActor = - currentBindingRevision?.primaryActorId || - bindingQuery.data?.primaryActorId || + const currentDefaultRouteActor = + currentDefaultRouteRevision?.primaryActorId || + defaultRouteQuery.data?.primaryActorId || ''; useEffect(() => { @@ -1143,9 +1147,9 @@ const ScopeInvokePage: React.FC = () => { ); const observedEventCount = observedEvents.length; - const bindingStatus = - bindingQuery.data?.deploymentStatus || - (bindingQuery.data?.available ? 'ready' : 'missing'); + const defaultRouteStatus = + defaultRouteQuery.data?.deploymentStatus || + (defaultRouteQuery.data?.available ? 'ready' : 'missing'); return ( @@ -1308,15 +1312,16 @@ const ScopeInvokePage: React.FC = () => { title={ } - subtitle="Published binding visible beside the playground." - title="Current Binding" + subtitle="Current default route visible beside the playground." + title="Current Default Route" /> } > - {!bindingQuery.data?.available || !currentBindingRevision ? ( + {!defaultRouteQuery.data?.available || + !currentDefaultRouteRevision ? ( ) : ( @@ -1325,31 +1330,31 @@ const ScopeInvokePage: React.FC = () => { - {bindingQuery.data.displayName || - bindingQuery.data.serviceId} + {defaultRouteQuery.data.displayName || + defaultRouteQuery.data.serviceId} } label="Target" - value={currentBindingTarget} + value={currentDefaultRouteTarget} /> } label="Revision" - value={currentBindingRevision.revisionId} + value={currentDefaultRouteRevision.revisionId} /> } label="Actor" - value={currentBindingActor || 'n/a'} + value={currentDefaultRouteActor || 'n/a'} /> - {currentBindingContext ? ( + {currentDefaultRouteContext ? ( - {currentBindingContext} + {currentDefaultRouteContext} ) : null} + ) : activeExecutionInteraction.kind === 'wait_signal' ? ( + ) : ( @@ -720,8 +678,8 @@ const ScopeBackedTeamCard: React.FC<{ ); }; -const ScopeBackedTeamRow: React.FC<{ - readonly preview: ScopeBackedTeamPreview; +const MemberRosterRow: React.FC<{ + readonly preview: MemberRosterPreview; }> = ({ preview }) => { const { token } = theme.useToken(); @@ -793,7 +751,7 @@ const ScopeBackedTeamRow: React.FC<{ fontSize: 13, }} > - 默认入口:{preview.entryLabel} + 成员标识:{preview.entryLabel} @@ -812,7 +770,7 @@ const ScopeBackedTeamRow: React.FC<{ onClick={stopEvent(() => history.push(preview.detailHref))} type="primary" > - 查看团队 + {preview.primaryActionLabel} @@ -887,16 +845,10 @@ const TeamsHomePage: React.FC = () => { history.replace(buildScopeHref("/teams", activeDraft)); }, [activeDraft]); - const bindingQuery = useQuery({ + const membersQuery = useQuery({ enabled: scopeId.length > 0, - queryKey: ["teams", "binding", scopeId], - queryFn: () => studioApi.getScopeBinding(scopeId), - retry: false, - }); - const workflowsQuery = useQuery({ - enabled: scopeId.length > 0, - queryKey: ["teams", "workflows", scopeId], - queryFn: () => scopesApi.listWorkflows(scopeId), + queryKey: ["teams", "members", scopeId], + queryFn: () => studioApi.listMembers(scopeId), retry: false, }); const servicesQuery = useQuery({ @@ -907,152 +859,120 @@ const TeamsHomePage: React.FC = () => { tenantId: scopeId, appId: scopeServiceAppId, namespace: scopeServiceNamespace, - }), + }), retry: false, }); - const matchedServiceIds = React.useMemo( - () => - collectWorkflowOperationalServiceIds({ - binding: bindingQuery.data ?? null, - services: servicesQuery.data ?? [], - workflows: workflowsQuery.data ?? [], - }), - [bindingQuery.data, servicesQuery.data, workflowsQuery.data], + const studioMembers = React.useMemo( + () => [...(membersQuery.data?.members ?? [])].sort(compareMembers), + [membersQuery.data?.members], ); - const scopePreviewServiceId = React.useMemo( + const runtimeTrackableMembers = React.useMemo( () => - bindingQuery.data?.available - ? trimOptional(bindingQuery.data?.serviceId) - : "", - [bindingQuery.data?.available, bindingQuery.data?.serviceId], + studioMembers.filter( + (member) => + Boolean(trimOptional(member.publishedServiceId)) || + Boolean(trimOptional(member.lastBoundRevisionId)), + ), + [studioMembers], ); - const runtimeServiceIds = React.useMemo(() => { - const normalizedScopePreviewServiceId = trimOptional(scopePreviewServiceId); - const ordered = normalizedScopePreviewServiceId - ? [ - normalizedScopePreviewServiceId, - ...matchedServiceIds.filter((serviceId) => serviceId !== normalizedScopePreviewServiceId), - ] - : matchedServiceIds; - return ordered; - }, [matchedServiceIds, scopePreviewServiceId]); - const runtimeSampleServiceIds = runtimeServiceIds.slice( - 0, - WORKFLOW_RUNTIME_GUARDRAIL, + const runtimeSampleMembers = React.useMemo( + () => runtimeTrackableMembers.slice(0, WORKFLOW_RUNTIME_GUARDRAIL), + [runtimeTrackableMembers], ); - const guardrailedServiceIds = React.useMemo( - () => new Set(runtimeServiceIds.slice(WORKFLOW_RUNTIME_GUARDRAIL)), - [runtimeServiceIds], + const guardrailedMemberIds = React.useMemo( + () => + new Set( + runtimeTrackableMembers + .slice(WORKFLOW_RUNTIME_GUARDRAIL) + .map((member) => trimOptional(member.memberId)) + .filter(Boolean), + ), + [runtimeTrackableMembers], ); - const serviceRunQueries = useQueries({ - queries: runtimeSampleServiceIds.map((serviceId) => ({ - enabled: scopeId.length > 0 && servicesQuery.isSuccess, - queryKey: ["teams", "runs", scopeId, serviceId], + const memberRunQueries = useQueries({ + queries: runtimeSampleMembers.map((member) => ({ + enabled: scopeId.length > 0 && membersQuery.isSuccess, + queryKey: ["teams", "member-runs", scopeId, member.memberId], queryFn: () => - scopeRuntimeApi.listServiceRuns(scopeId, serviceId, { + scopeRuntimeApi.listMemberRuns(scopeId, member.memberId, { take: 12, }), retry: false, })), }); - const runtimeAvailableByServiceId = React.useMemo(() => { + const runtimeAvailableByMemberId = React.useMemo(() => { const available = new Set(); - serviceRunQueries.forEach((query, index) => { + memberRunQueries.forEach((query, index) => { if (query.isSuccess) { - available.add(runtimeSampleServiceIds[index] ?? ""); + available.add(trimOptional(runtimeSampleMembers[index]?.memberId)); } }); return available; - }, [runtimeSampleServiceIds, serviceRunQueries]); - const runsByServiceId = React.useMemo( + }, [memberRunQueries, runtimeSampleMembers]); + const runsByMemberId = React.useMemo( () => Object.fromEntries( - runtimeSampleServiceIds.map((serviceId, index) => [ - serviceId, - serviceRunQueries[index]?.data?.runs ?? [], + runtimeSampleMembers.map((member, index) => [ + trimOptional(member.memberId), + memberRunQueries[index]?.data?.runs ?? [], ]), ) as Record, - [runtimeSampleServiceIds, serviceRunQueries], - ); - const units = React.useMemo( - () => - buildWorkflowOperationalUnits({ - binding: bindingQuery.data ?? null, - runsByServiceId, - services: servicesQuery.data ?? [], - signals: { - runtimeAvailableByServiceId, - runtimeGuardrailedServiceIds: guardrailedServiceIds, - servicesAvailable: servicesQuery.isSuccess, - }, - workflows: workflowsQuery.data ?? [], - }), - [ - bindingQuery.data, - guardrailedServiceIds, - runsByServiceId, - runtimeAvailableByServiceId, - servicesQuery.data, - servicesQuery.isSuccess, - workflowsQuery.data, - ], + [memberRunQueries, runtimeSampleMembers], ); - const scopePreviewTeam = React.useMemo( + const memberPreviews = React.useMemo( () => - buildScopeBackedTeamPreview({ - binding: bindingQuery.data ?? null, - guardrailedServiceIds, - runsByServiceId, - runtimeAvailableByServiceId, - scopeId, - services: servicesQuery.data ?? [], - workflows: workflowsQuery.data ?? [], - }), + studioMembers.map((member) => + buildMemberRosterPreview({ + guardrailedMemberIds, + member, + runsByMemberId, + runtimeAvailableByMemberId, + scopeId, + services: servicesQuery.data ?? [], + }), + ), [ - bindingQuery.data, - guardrailedServiceIds, - runsByServiceId, - runtimeAvailableByServiceId, + guardrailedMemberIds, + runsByMemberId, + runtimeAvailableByMemberId, scopeId, servicesQuery.data, - workflowsQuery.data, + studioMembers, ], ); - - const draftUnits = units.filter( - (unit) => unit.isDraftOnly || (!unit.matchedService && !unit.latestRun), + const membersPendingBindingCount = React.useMemo( + () => + studioMembers.filter( + (member) => + !trimOptional(member.publishedServiceId) || + !trimOptional(member.lastBoundRevisionId), + ).length, + [studioMembers], ); - const visibleTeamCount = scopePreviewTeam ? 1 : 0; - const scopeBindingUnavailableNotice = - scopeId.length > 0 && - bindingQuery.isSuccess && - bindingQuery.data?.available === false - ? { - description: - "没有找到已发布的默认入口服务,所以首页暂时没有运行信号。去 Studio 发布团队后,这里会自动出现。", - title: "当前 Scope 还没有默认团队入口", - } - : null; + const visibleTeamCount = memberPreviews.length; const resolvedRosterView = manualRosterView ?? (visibleTeamCount >= compactTeamRosterThreshold ? "list" : "cards"); const useCompactRoster = resolvedRosterView === "list"; - const healthyTeamCount = scopePreviewTeam?.attention === "healthy" ? 1 : 0; - const attentionTeamCount = - scopePreviewTeam && scopePreviewTeam.attention !== "healthy" ? 1 : 0; - const draftHint = - draftUnits.length > 0 - ? `当前 Scope 里还有 ${draftUnits.length} 个已保存的行为定义,但它们还没有形成首页团队入口。` - : "当前 Scope 里还没有形成首页团队入口。"; + const healthyTeamCount = memberPreviews.filter( + (preview) => preview.attention === "healthy", + ).length; + const attentionTeamCount = memberPreviews.filter( + (preview) => preview.attention !== "healthy", + ).length; + const emptyRosterHint = + scopeId.length > 0 + ? "当前 Scope 下还没有创建任何 member。进入 Studio 创建成员后,这里会按成员逐个展示。" + : "先导入一个 Scope,首页才能渲染出这组成员卡片。"; const partialIssues = [ servicesQuery.isError ? "服务目录暂时不可见。" : null, - bindingQuery.isError ? "当前 Scope 的团队绑定信息暂时不可见。" : null, - ...serviceRunQueries.map((query) => - query.isError ? "部分运行信号暂时不可见。" : null, + membersQuery.isError ? "当前 Scope 的成员清单暂时不可见。" : null, + ...memberRunQueries.map((query) => + query.isError ? "部分成员运行信号暂时不可见。" : null, ), - guardrailedServiceIds.size > 0 - ? `当前首页只采样前 ${WORKFLOW_RUNTIME_GUARDRAIL} 个服务的运行信号。` + guardrailedMemberIds.size > 0 + ? `当前首页只采样前 ${WORKFLOW_RUNTIME_GUARDRAIL} 个已绑定成员的运行信号。` : null, ].filter((issue): issue is string => Boolean(issue)); @@ -1206,7 +1126,7 @@ const TeamsHomePage: React.FC = () => { {scopeId} - 首页按这个 Scope 汇总已经形成入口的团队,Scope 只做上下文,不再直接当团队名展示。 + 首页按这个 Scope 汇总成员本身的绑定与运行状态,Scope 只做上下文,不再直接当团队名展示。 @@ -1215,7 +1135,7 @@ const TeamsHomePage: React.FC = () => { {!scopeId ? ( ) : null} @@ -1246,15 +1166,6 @@ const TeamsHomePage: React.FC = () => { /> ) : null} - {scopeBindingUnavailableNotice ? ( - - ) : null} - {scopeId ? ( <>
{ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", }} > - +
- {draftUnits.length > 0 ? ( + {membersPendingBindingCount > 0 ? ( { 打开 Studio } - description={`其中 ${draftUnits.length} 个行为定义还停留在草稿阶段,尚未形成首页团队入口。`} + description={`其中 ${membersPendingBindingCount} 个成员还没有完成独立绑定,或还没有形成稳定的可调用入口。`} showIcon - title="还有草稿待整理" + title="还有成员待整理" type="info" /> ) : null} - {workflowsQuery.isLoading ? ( - - ) : workflowsQuery.isError ? ( + {membersQuery.isLoading ? ( + + ) : membersQuery.isError ? ( - ) : scopePreviewTeam ? ( + ) : memberPreviews.length > 0 ? ( <>
{ margin: 0, }} > - 团队入口 + 团队成员 - 当前 Scope 下已经形成首页入口的团队。 + 当前 Scope 下已经登记的成员,以及它们各自的绑定和运行状态。
{visibleTeamCount > 1 ? ( @@ -1361,7 +1272,9 @@ const TeamsHomePage: React.FC = () => { gap: 14, }} > - + {memberPreviews.map((preview) => ( + + ))} ) : (
{ gridTemplateColumns: "repeat(auto-fit, minmax(340px, 1fr))", }} > - + {memberPreviews.map((preview) => ( + + ))}
)} ) : (