From 3cb0039fedfc7c2da91347b3623d2e1b635313ec Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 8 Jul 2026 10:16:17 -0600 Subject: [PATCH 1/2] fix(desktop): resolve dotted-basename imports in the unit test loader resolveSourcePath treated any dotted basename as already-extensioned (path.extname('.../ProfileAvatarEditor.utils') is '.utils'), handing node an unresolvable path. Any test importing the dialog component tree died on AgentCreationPreview -> ProfileAvatarEditor.utils. Decide by file existence instead, falling back to default resolution so genuinely-extensioned specifiers (.mjs/.json/explicit .ts) resolve exactly as before. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes --- desktop/test-loader-hooks.mjs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/desktop/test-loader-hooks.mjs b/desktop/test-loader-hooks.mjs index 73c8fa643..e033e05dd 100644 --- a/desktop/test-loader-hooks.mjs +++ b/desktop/test-loader-hooks.mjs @@ -14,7 +14,10 @@ const repoRoot = path.resolve( ); function resolveSourcePath(basePath) { - if (path.extname(basePath)) { + // Existence decides, not path.extname — a dotted basename like + // `ProfileAvatarEditor.utils` (→ .utils.ts on disk) looks like an + // extension but still needs resolving. + if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) { return basePath; } @@ -32,7 +35,7 @@ function resolveSourcePath(basePath) { } } - return `${basePath}.ts`; + return null; } // emoji-mart ships a bundled CJS main that node's cjs-module-lexer cannot @@ -70,22 +73,26 @@ export function resolve(specifier, context, nextResolve) { // Otherwise paths like `@/.../foo.mjs` would be coerced into `foo.mjs.ts` // and fail to resolve. const resolved = resolveSourcePath(`${srcRoot}/${stripped}`); - return nextResolve(resolved, context); + return nextResolve(resolved ?? `${srcRoot}/${stripped}`, context); } // Resolve extensionless relative TS imports (e.g. `./parseImeta`) — the app's // bundler adds the extension, but node's ESM resolver does not. Without this, // any .ts that relative-imports a sibling .ts can't be imported from a test, // which previously forced stale inlined copies of the source under test. + // Dotted basenames (`./ProfileAvatarEditor.utils`) look like extensions to + // path.extname, so resolveSourcePath existence-checks instead. if ( (specifier.startsWith("./") || specifier.startsWith("../")) && - !path.extname(specifier) && - context.parentURL + context.parentURL?.startsWith("file:") ) { const parentPath = fileURLToPath(context.parentURL); const resolved = resolveSourcePath( path.resolve(path.dirname(parentPath), specifier), ); - return nextResolve(resolved, context); + if (resolved) { + return nextResolve(resolved, context); + } + return nextResolve(specifier, context); } return nextResolve(specifier, context); } From 0a18df913a1bacb0ae63589fe006000899f3ec71 Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 8 Jul 2026 10:16:27 -0600 Subject: [PATCH 2/2] refactor(desktop): route definition-edit through AgentDialog (Phase 1B.3c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentDialog gains a definition-edit union arm — a pass-through to AgentDefinitionDialog driven by the caller's PersonaDialogState (edit/ duplicate/import). The two remaining direct AgentDefinitionDialog mounts re-point through it (UserProfilePersonaDialogs, AgentsView), making AgentDialog the single dialog entry point for every intent: create-definition(+/-start), create-instance, edit-instance, edit-definition. AgentDefinitionDialog is now internal to the dialog family. New routing pinning test covers exact prop pass-through per arm. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes --- .../src/features/agents/ui/AgentDialog.tsx | 45 ++++++++-- desktop/src/features/agents/ui/AgentsView.tsx | 7 +- .../agents/ui/agentDialogRouting.test.mjs | 90 +++++++++++++++++++ .../profile/ui/UserProfilePersonaDialogs.tsx | 5 +- 4 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 desktop/src/features/agents/ui/agentDialogRouting.test.mjs diff --git a/desktop/src/features/agents/ui/AgentDialog.tsx b/desktop/src/features/agents/ui/AgentDialog.tsx index 7d76d2501..1d2f0e856 100644 --- a/desktop/src/features/agents/ui/AgentDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDialog.tsx @@ -42,15 +42,42 @@ type AgentDialogInstanceEditProps = { onUpdated?: (agent: ManagedAgent) => void; }; -type AgentDialogProps = AgentDialogCreateProps | AgentDialogInstanceEditProps; +type AgentDialogDefinitionEditProps = { + mode: "definition-edit"; + open: boolean; + title: string; + description: string; + submitLabel: string; + initialValues: CreatePersonaInput | UpdatePersonaInput | null; + error: Error | null; + isPending: boolean; + isImportPending?: boolean; + runtimes: AcpRuntimeCatalogEntry[]; + runtimesLoading?: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: ( + input: CreatePersonaInput | UpdatePersonaInput, + ) => Promise; + onImportUpdateFile?: ( + personaId: string, + fileBytes: number[], + fileName: string, + ) => Promise; +}; + +type AgentDialogProps = + | AgentDialogCreateProps + | AgentDialogInstanceEditProps + | AgentDialogDefinitionEditProps; /** - * Unified entry point (Phase 1B.2/1B.3b): routes an intent to the form that - * owns it. The definition family renders AgentDefinitionDialog in create mode - * with a "start after create" toggle; the standalone-instance intent renders - * CreateAgentDialog unchanged; instance-edit renders AgentInstanceEditDialog - * (persistent mount + `open` toggle — its reset lifecycle is keyed on - * [open, agent.pubkey]). Physical consolidation of the forms is Phase 1B.3c+. + * Unified entry point (Phase 1B.2/1B.3b/1B.3c): routes an intent to the form + * that owns it. The definition family renders AgentDefinitionDialog — create + * mode adds a "start after create" toggle, definition-edit passes the caller's + * PersonaDialogState-derived props through unchanged (edit/duplicate/import). + * The standalone-instance intent renders CreateAgentDialog unchanged; + * instance-edit renders AgentInstanceEditDialog (persistent mount + `open` + * toggle — its reset lifecycle is keyed on [open, agent.pubkey]). */ export function AgentDialog(props: AgentDialogProps) { if (props.mode === "instance-edit") { @@ -63,6 +90,10 @@ export function AgentDialog(props: AgentDialogProps) { /> ); } + if (props.mode === "definition-edit") { + const { mode: _mode, ...definitionProps } = props; + return ; + } return ; } diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 0a5ccf7b6..c9ea9da75 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -8,7 +8,6 @@ import { AddTeamToChannelDialog } from "./AddTeamToChannelDialog"; import { AgentDialog, type AgentDialogCreateMode } from "./AgentDialog"; import { BatchImportDialog } from "./BatchImportDialog"; import { PersonaCatalogDialog } from "./PersonaCatalogDialog"; -import { AgentDefinitionDialog } from "./AgentDefinitionDialog"; import { PersonaDeleteDialog } from "./PersonaDeleteDialog"; import { PersonaImportUpdateDialog } from "./PersonaImportUpdateDialog"; import { PersonaShareDialog } from "./PersonaShareDialog"; @@ -30,8 +29,7 @@ export function AgentsView() { const agents = useManagedAgentActions(); const personas = usePersonaActions(); // Exclusivity: create never sets `personaDialogState` (edit/dup/import do), - // so the unified create dialog and the edit/dup/import AgentDefinitionDialog - // mount never coexist. + // so the create-mode and definition-edit AgentDialog mounts never coexist. const [createDialogMode, setCreateDialogMode] = React.useState(null); @@ -241,7 +239,7 @@ export function AgentsView() { /> ) : null} {personas.personaDialogState ? ( - {}; + +test("definition-edit routes to AgentDefinitionDialog with exact pass-through", () => { + const props = { + description: "Edit the agent definition.", + error: null, + initialValues: { displayName: "Brain" }, + isImportPending: true, + isPending: false, + onImportUpdateFile: async () => {}, + onOpenChange: noop, + onSubmit: async () => {}, + open: true, + runtimes: [], + runtimesLoading: false, + submitLabel: "Save", + title: "Edit agent", + }; + + const element = AgentDialog({ mode: "definition-edit", ...props }); + + assert.equal(element.type, AgentDefinitionDialog); + assert.deepEqual(element.props, props, "props must pass through unchanged"); + assert.equal( + "mode" in element.props, + false, + "the mode discriminant must not leak into AgentDefinitionDialog", + ); +}); + +test("instance-edit routes to AgentInstanceEditDialog with its contract props", () => { + const agent = { pubkey: "abc", name: "test-agent" }; + const onOpenChange = noop; + const onUpdated = noop; + + const element = AgentDialog({ + mode: "instance-edit", + agent, + onOpenChange, + onUpdated, + open: true, + }); + + assert.equal(element.type, AgentInstanceEditDialog); + assert.deepEqual(element.props, { + agent, + onOpenChange, + onUpdated, + open: true, + }); +}); + +test("create modes route to the internal create router, not a form directly", () => { + for (const mode of ["definition", "instance"]) { + const element = AgentDialog({ + mode, + definitionError: null, + isDefinitionPending: false, + onInstanceCreated: noop, + onOpenChange: noop, + onSubmitDefinition: async () => true, + runtimes: [], + runtimesLoading: false, + }); + + assert.notEqual(element.type, AgentDefinitionDialog); + assert.notEqual(element.type, AgentInstanceEditDialog); + assert.equal( + typeof element.type, + "function", + `${mode} must route through the internal create router`, + ); + assert.equal(element.type.name, "AgentCreateDialogRouter"); + } +}); diff --git a/desktop/src/features/profile/ui/UserProfilePersonaDialogs.tsx b/desktop/src/features/profile/ui/UserProfilePersonaDialogs.tsx index 57d89adc9..84896a38d 100644 --- a/desktop/src/features/profile/ui/UserProfilePersonaDialogs.tsx +++ b/desktop/src/features/profile/ui/UserProfilePersonaDialogs.tsx @@ -5,7 +5,7 @@ import type { UpdatePersonaInput, } from "@/shared/api/types"; import { PersonaDeleteDialog } from "@/features/agents/ui/PersonaDeleteDialog"; -import { AgentDefinitionDialog } from "@/features/agents/ui/AgentDefinitionDialog"; +import { AgentDialog } from "@/features/agents/ui/AgentDialog"; import type { PersonaDialogState } from "@/features/agents/ui/personaDialogState"; export function UserProfilePersonaDialogs({ @@ -35,11 +35,12 @@ export function UserProfilePersonaDialogs({ }) { return ( <> - {