From beecffd45bdb6fde159d5c3b253c5af09d109351 Mon Sep 17 00:00:00 2001 From: potter Date: Mon, 27 Apr 2026 16:35:33 +0800 Subject: [PATCH 1/3] fix: clarify studio member creation flow --- .../src/pages/studio/index.test.tsx | 45 ++- .../src/pages/studio/index.tsx | 100 +++-- ...-script-member-flow-implementation-plan.md | 356 ++++++++++++++++++ 3 files changed, 465 insertions(+), 36 deletions(-) create mode 100644 docs/design/2026-04-27-studio-script-member-flow-implementation-plan.md diff --git a/apps/aevatar-console-web/src/pages/studio/index.test.tsx b/apps/aevatar-console-web/src/pages/studio/index.test.tsx index a45826629..dfe014e4b 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.test.tsx @@ -2964,10 +2964,21 @@ describe("StudioPage", () => { }); it("shows script and gagent as member kinds before their create APIs land", async () => { + (studioApi.getAppContext as jest.Mock).mockResolvedValueOnce({ + ...defaultStudioAppContext, + scopeId: "scope-1", + scopeResolved: true, + scriptStorageMode: "scope", + features: { + ...defaultStudioAppContext.features, + scripts: true, + }, + }); + renderStudioPage("/studio?focus=workflow%3Aworkflow-1&tab=studio"); - fireEvent.click(await screen.findByLabelText("Create member")); - const createDialog = await screen.findByRole("dialog", { name: "Create member" }); + fireEvent.click(await screen.findByRole("button", { name: "Create member" })); + let createDialog = await screen.findByRole("dialog", { name: "Create member" }); const scriptChip = within(createDialog).getByRole("button", { name: "Create Script member", @@ -2975,12 +2986,25 @@ describe("StudioPage", () => { fireEvent.click(scriptChip); expect(scriptChip).toHaveAttribute("aria-pressed", "true"); + expect(within(createDialog).queryByLabelText("Member name")).toBeNull(); expect( screen.getByText( - "Script member creation still relies on the upcoming member API. For now, continue in Build > Script to inspect or edit script implementations.", + "Script member creation still relies on the upcoming member API. Open Build > Script to inspect, edit, save, and prepare script implementations for binding.", ), ).toBeTruthy(); - expect(within(createDialog).getByRole("button", { name: "Create member" })).toBeDisabled(); + fireEvent.click( + within(createDialog).getByRole("button", { name: "Open Script builder" }), + ); + + expect(await screen.findByTestId("studio-script-build-panel")).toBeTruthy(); + await waitFor(() => { + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get("tab")).toBe("scripts"); + expect(searchParams.get("step")).toBe("build"); + }); + + fireEvent.click(await screen.findByRole("button", { name: "Create member" })); + createDialog = await screen.findByRole("dialog", { name: "Create member" }); const gagentChip = within(createDialog).getByRole("button", { name: "Create GAgent member", @@ -2988,11 +3012,22 @@ describe("StudioPage", () => { fireEvent.click(gagentChip); expect(gagentChip).toHaveAttribute("aria-pressed", "true"); + expect(within(createDialog).queryByLabelText("Member name")).toBeNull(); expect( screen.getByText( - "GAgent member creation still relies on the upcoming member API. For now, continue in Build > GAgent to inspect or edit GAgent implementations.", + "GAgent member creation still relies on the upcoming member API. Open Build > GAgent to select, inspect, and prepare GAgent implementations for binding.", ), ).toBeTruthy(); + fireEvent.click( + within(createDialog).getByRole("button", { name: "Open GAgent builder" }), + ); + + expect(await screen.findByTestId("studio-gagent-build-panel")).toBeTruthy(); + await waitFor(() => { + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get("tab")).toBe("gagents"); + expect(searchParams.get("step")).toBe("build"); + }); }); it("renames a workflow member from the inventory actions", async () => { diff --git a/apps/aevatar-console-web/src/pages/studio/index.tsx b/apps/aevatar-console-web/src/pages/studio/index.tsx index 329f3948e..873b4b63a 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.tsx @@ -3085,13 +3085,41 @@ const StudioPage: React.FC = () => { setCreateMemberModalOpen(false); }, [inventoryBusyKey]); - const handleCreateMember = useCallback(async () => { - if (createMemberKind !== 'workflow') { - void message.info( - createMemberKind === 'script' - ? 'Script member creation will move into this modal after the member API lands. For now, continue in Build > Script.' - : 'GAgent member creation will move into this modal after the member API lands. For now, continue in Build > GAgent.', + const handleCreateMember = useCallback(async (selectedCreateMemberKind: BuildMode) => { + if (selectedCreateMemberKind !== 'workflow') { + if (selectedCreateMemberKind === 'script' && !appContextQuery.data?.features.scripts) { + void message.warning('Script builder is not enabled for this workspace.'); + return; + } + + if (!(await confirmScriptsStudioLeave())) { + return; + } + + setCreateMemberModalOpen(false); + if (selectedCreateMemberKind === 'script') { + history.push( + buildStudioRoute({ + scopeId: resolvedStudioScopeId || undefined, + step: 'build', + tab: 'scripts', + }), + ); + setBuildSurface('scripts'); + setStudioSurface('build'); + return; + } + + history.push( + buildStudioRoute({ + scopeId: resolvedStudioScopeId || undefined, + step: 'build', + tab: 'gagents', + }), ); + setBuildSurface('gagent'); + setStudioSurface('build'); + void message.info('Opened GAgent builder.'); return; } @@ -3146,9 +3174,11 @@ const StudioPage: React.FC = () => { } }, [ applySavedWorkflowSelection, - createMemberKind, + appContextQuery.data?.features.scripts, + confirmScriptsStudioLeave, createMemberDirectoryId, createMemberName, + history, inventoryDirectoryId, resolvedStudioScopeId, visibleWorkflowSummaries, @@ -5984,16 +6014,22 @@ const StudioPage: React.FC = () => { open={createMemberModalOpen} title="Create member" onCancel={closeCreateMemberFlow} - onOk={() => void handleCreateMember()} - okText="Create member" + onOk={() => void handleCreateMember(createMemberKind)} + okText={ + createMemberKind === 'workflow' + ? 'Create member' + : createMemberKind === 'script' + ? 'Open Script builder' + : 'Open GAgent builder' + } okButtonProps={{ disabled: inventoryBusyAction === 'create' || - !trimOptional(createMemberName) || - createMemberKind !== 'workflow' || - !trimOptional( - trimOptional(createMemberDirectoryId) || inventoryDirectoryId, - ), + (createMemberKind === 'workflow' && + (!trimOptional(createMemberName) || + !trimOptional( + trimOptional(createMemberDirectoryId) || inventoryDirectoryId, + ))), loading: inventoryBusyAction === 'create', }} cancelButtonProps={{ @@ -6035,29 +6071,31 @@ const StudioPage: React.FC = () => { ))}
- Choose the implementation kind first. Workflow entry is - available now; Script and GAgent member creation will move into - this modal when the member API lands. + Choose the implementation kind first. Workflow creates a blank + draft here; Script and GAgent open their Build workspaces until + their member creation APIs land.
- + {createMemberKind === 'workflow' ? ( + + ) : null}
{createMemberKind === 'workflow' ? 'Workflow members currently start from a blank workflow draft with an empty canvas, so you can name it first and then continue editing inside Build.' : createMemberKind === 'script' - ? 'Script member creation still relies on the upcoming member API. For now, continue in Build > Script to inspect or edit script implementations.' - : 'GAgent member creation still relies on the upcoming member API. For now, continue in Build > GAgent to inspect or edit GAgent implementations.'} + ? 'Script member creation still relies on the upcoming member API. Open Build > Script to inspect, edit, save, and prepare script implementations for binding.' + : 'GAgent member creation still relies on the upcoming member API. Open Build > GAgent to select, inspect, and prepare GAgent implementations for binding.'}
{createMemberKind === 'workflow' ? (