diff --git a/desktop/src/features/agents/ui/AgentEditDialog.tsx b/desktop/src/features/agents/ui/AgentEditDialog.tsx
index bf8a09888d..74c86392dc 100644
--- a/desktop/src/features/agents/ui/AgentEditDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentEditDialog.tsx
@@ -47,8 +47,10 @@ import type {
} from "@/shared/api/types";
import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent";
import type { AgentDefinitionSubmitOptions } from "./AgentDefinitionDialog";
+import { runLocationForBackend } from "@/features/agents/lib/agentAccessWarning";
import { AgentDefinitionDialog } from "./AgentDefinitionDialog";
import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog";
+import { AgentRunLocationProvider } from "./AgentRunLocationContext";
import { editPersonaDialogState } from "./personaDialogState";
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -103,18 +105,26 @@ export function AgentEditDialog({
// parallelism, env vars, harness pin, auto-restart, start-on-launch, instance
// name) and owns a well-tested save path. Route all instance-present contexts
// here so no I/L field is accidentally omitted.
+ //
+ // Wrap in AgentRunLocationProvider so RespondToField's access warning can
+ // name the machine the agent actually runs on (mirrors AgentDialog's instance-
+ // edit arm — backend → run location for the remote-backend warning path).
if (ctx.kind === "instance-with-definition" || ctx.kind === "instance-only") {
return (
-
+
+
+
);
}
diff --git a/desktop/src/features/agents/ui/agentDialogRouting.test.mjs b/desktop/src/features/agents/ui/agentDialogRouting.test.mjs
index 15b6b7cf87..ecf1d68bc7 100644
--- a/desktop/src/features/agents/ui/agentDialogRouting.test.mjs
+++ b/desktop/src/features/agents/ui/agentDialogRouting.test.mjs
@@ -5,6 +5,7 @@ import { AgentDialog } from "./AgentDialog.tsx";
import { AgentDefinitionDialog } from "./AgentDefinitionDialog.tsx";
import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog.tsx";
import { AgentRunLocationProvider } from "./AgentRunLocationContext.tsx";
+import { AgentEditDialog } from "./AgentEditDialog.tsx";
// ── Phase 1B.3c routing pinning ─────────────────────────────────────────────
//
@@ -110,3 +111,124 @@ test("create mode routes to the internal create router, not a form directly", ()
);
assert.equal(element.type.name, "AgentCreateDialogRouter");
});
+
+// ── Phase 1 merged surface routing (AgentEditDialog) ────────────────────────
+//
+// AgentEditDialog collapses R1–R7 onto one entry point. Like AgentDialog, the
+// instance-present arms are hook-free so they can be called directly and the
+// returned element tree inspected. The definition-only arm delegates to a
+// sub-component that owns hooks, so we only confirm the type name there.
+//
+// These tests prove:
+// • instance-with-definition → AgentInstanceEditDialog (all I/L fields live here)
+// • instance-only → AgentInstanceEditDialog (unlinked agents save correctly)
+// • definition-only → AgentEditDefinitionOnlyDialog sub-component
+//
+// They also confirm the AgentRunLocationProvider wrapper is present on instance
+// paths (so RespondToField's remote-backend warning works).
+
+const minimalDefinition = {
+ id: "def-abc",
+ displayName: "Bot",
+ avatarUrl: "",
+ systemPrompt: "Do stuff.",
+ runtime: "goose",
+ model: null,
+ provider: null,
+ isBuiltIn: false,
+ isActive: true,
+ namePool: [],
+ envVars: {},
+ respondTo: null,
+ respondToAllowlist: [],
+ parallelism: null,
+ createdAt: "2025-01-01T00:00:00Z",
+ updatedAt: "2025-01-01T00:00:00Z",
+};
+
+const minimalInstance = {
+ pubkey: "pk-0000",
+ name: "Bot",
+ avatarUrl: "",
+ systemPrompt: "Do stuff.",
+ model: null,
+ provider: null,
+ envVars: {},
+ respondTo: null,
+ respondToAllowlist: [],
+ parallelism: null,
+ autoRestartOnConfigChange: false,
+ startOnAppLaunch: false,
+};
+
+test("AgentEditDialog: instance-with-definition routes instance to AgentInstanceEditDialog", () => {
+ const ctx = {
+ kind: "instance-with-definition",
+ definition: minimalDefinition,
+ instance: minimalInstance,
+ };
+
+ const element = AgentEditDialog({
+ ctx,
+ open: true,
+ onOpenChange: noop,
+ });
+
+ // Wrapped in AgentRunLocationProvider for respondTo warning.
+ assert.equal(element.type, AgentRunLocationProvider);
+ const form = element.props.children;
+ assert.equal(
+ form.type,
+ AgentInstanceEditDialog,
+ "linked agent edit must reach AgentInstanceEditDialog (all I/L fields)",
+ );
+ assert.equal(form.props.agent, minimalInstance);
+ assert.equal(form.props.open, true);
+ assert.equal(
+ form.props.onEditLinkedPersona,
+ undefined,
+ "R4 back-door must be deleted",
+ );
+});
+
+test("AgentEditDialog: instance-only routes to AgentInstanceEditDialog (unlinked save)", () => {
+ const ctx = { kind: "instance-only", instance: minimalInstance };
+
+ const element = AgentEditDialog({
+ ctx,
+ open: true,
+ onOpenChange: noop,
+ });
+
+ // Wrapped in AgentRunLocationProvider for respondTo warning.
+ assert.equal(element.type, AgentRunLocationProvider);
+ const form = element.props.children;
+ assert.equal(
+ form.type,
+ AgentInstanceEditDialog,
+ "unlinked agent edit must reach AgentInstanceEditDialog (UpdateManagedAgentInput path)",
+ );
+ assert.equal(form.props.agent, minimalInstance);
+});
+
+test("AgentEditDialog: definition-only routes to definition sub-component", () => {
+ const ctx = { kind: "definition-only", definition: minimalDefinition };
+
+ const element = AgentEditDialog({
+ ctx,
+ open: true,
+ onOpenChange: noop,
+ });
+
+ // Hook-bearing sub-component: confirm by name rather than reference.
+ assert.equal(
+ typeof element.type,
+ "function",
+ "definition-only must route to a React component",
+ );
+ assert.equal(
+ element.type.name,
+ "AgentEditDefinitionOnlyDialog",
+ "definition-only must use the coordinator-wired sub-component",
+ );
+});