diff --git a/DEVLOG.md b/DEVLOG.md index 27569f9c..28b88532 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -13312,3 +13312,39 @@ production Playwright build passed all 45 desktop, mobile, accessibility, and application journeys. Before/after evidence plus the UX health report are preserved under `docs/audits/agent-studio-2026-07-15/`. Production verification follows after tagging and deployment. + +--- + +## 2026-07-15 — Mission Control agent fleet management + +Promoted Mission Control from a read-only agent summary to the primary fleet +control plane. The new `/agents` and `/agents/[id]` routes own global profile +creation/editing, the single execution-runtime assignment, MCP-client rollups, +workspace filtering and bind/unbind actions, recent work, and permission-aware +archive/removal. Legacy Agent Studio URLs redirect to the new routes. + +Binding writes name one workspace explicitly, re-check owner/admin membership +server-side, record the same durable audit/activity events as workspace writes, +and preserve archived bindings for reversible re-adoption. Workspace Agent +settings are now policy-only (capacity, capability overrides, routing, +engagement, and approval); Instance Admin is governance-only (request approval, +instance sharing, force-disable, audit/recovery) and links to Mission Control +for routine management. Instance admins get smart removal there, while profile +owners get archive and workspace admins can only unbind. + +The global shell now labels ordinary aggregate pages as read-only and the Agent +fleet as a Global control plane. Navigation, account settings, workspace copy, +current guides, router integration coverage, and Playwright lifecycle/redirect +coverage were updated to reflect Define → Bind → Operate, with Govern reserved +for instance administrators. + +Follow-up coverage moved the older multi-workspace binding and mobile smoke +contracts onto the Mission Control fleet routes. MCP-client creation is now +shown per binding only when the caller can administer that workspace, matching +the server-side credential authorization boundary. + +Verification: typecheck and lint passed (existing repository warnings only); +the full Vitest gate passed 1,320 tests with one intentional live-connector +skip; the production build succeeded; the affected mobile and multi-workspace +specs passed 13 journeys; and the complete serial Playwright gate passed all 47 +desktop, mobile, accessibility, and application journeys. diff --git a/docs/agents/overview.md b/docs/agents/overview.md index df63aad6..8f38d10a 100644 --- a/docs/agents/overview.md +++ b/docs/agents/overview.md @@ -8,10 +8,11 @@ lifecycle of an agent, the implicit heartbeat that comes from webhook delivery, and where to find each piece of the surface in the app. ::: info Configuration map -Use **Agent Studio** for identity, prompt, and the single primary execution -runtime; **Workspace → Agent roster** for binding policy; **Workspace → Agent -access** for one or more MCP client credentials; **Instance Administration** -for global governance; and **Mission Control** for read-only operations. +Use **Mission Control → Agents** to define identities, choose the single primary +execution runtime, bind workspaces, and monitor recent work; **Workspace → Agent +policy** for local capacity/routing/engagement/approval; **Workspace → Agent +access** for one or more MCP client credentials; and **Instance Administration** +for approval, sharing, force-disable, audit, and recovery. ::: ## Two ways to run agent work @@ -163,13 +164,14 @@ opening each agent's detail page. A typical onboarding sequence: -1. **Define the identity.** Agent Studio → New profile starts with Hermes, +1. **Define the identity.** Mission Control → Agents → New profile starts with Hermes, Claude, Codex, or custom. Set `name`, `profileKey`, description, avatar, provider, and execution engine once. 2. **Choose the primary runtime.** A profile can select zero or one execution host. Active workspace bindings inherit later identity/execution edits. -3. **Bind it to a workspace.** The workspace roster owns local capacity, - routing eligibility, engagement, approval, and capability overrides. +3. **Bind it to a workspace.** Mission Control owns binding and unbinding; + workspace settings own local capacity, routing eligibility, engagement, + approval, and capability overrides. 4. **Declare capabilities.** Lowercase, free-form. Common entries match priority names (`urgent`, `high`) for `PRIORITY_MATCH` and label names (`infra`, `frontend`) for `CAPABILITY_MATCH`. @@ -370,7 +372,11 @@ without leaving the current page. - **Agent detail page** — `/w//agents/`. Pipeline (assigned-but-not- yet-acked → in-progress → recent), timeline (assignment, ack, transition, comment events), uptime sparkline, webhook delivery health. -- **Settings → Agents** — `/w//settings/agents`. Create, edit, archive. +- **Mission Control → Agents** — `/agents`. Create and edit profiles, attach + runtimes and MCP clients, bind/unbind workspaces, inspect recent runs, and + archive or safely remove identities. +- **Settings → Agent policy** — `/w//settings/agents`. Workspace-local + capacity, routing eligibility, engagement, approval, and capabilities. - **Settings → Workspace** — `/w//settings/workspace`. The dispatch knobs that govern agent selection. - **Settings → Dispatch Rules** — declarative routing, evaluated before diff --git a/docs/agents/profiles-and-bindings.md b/docs/agents/profiles-and-bindings.md index d949269b..3aa31325 100644 --- a/docs/agents/profiles-and-bindings.md +++ b/docs/agents/profiles-and-bindings.md @@ -25,11 +25,11 @@ template. It's owned by a user and lives outside any workspace. `profileKey` is **unique per owner**, so two different users can each own a `victor` without colliding. -Manage profiles in **Agent Studio** at **`/settings/agents`** (the global, -account-level agents page). Its detail view reports runtime, workspace-binding, -and MCP-client readiness and is the only place identity/execution fields are -edited. This is also where your profiles surface on -[Mission Control](/guide/mission-control.html#my-agents). +Manage profiles in **Mission Control → Agents** at **`/agents`**. Its detail +view reports runtime, workspace-binding, MCP-client readiness, and recent work; +it is the primary place to create, edit, connect, bind, archive, or safely +remove an identity. The former `/settings/agents` and +`/settings/agents/[id]` routes redirect here so bookmarks remain valid. Updating a profile synchronizes its identity and execution fields into every active workspace binding. Binding policy such as capacity and capability @@ -49,19 +49,17 @@ workspace, then layers per-workspace **policy** on top: | Engagement mode | `engagementMode` | Per-binding default; null = inherit `Workspace.assignmentEngagementMode` | | Approval gate | `requireApprovalBeforeStart` | Per-binding override of the workspace approval gate | -Bind, set policy, and unbind from the workspace agents page at -**`/w/[slug]/settings/agents`**. The **catalog** there lists the -profiles available to bind (ones you own, plus instance-shared ones) -that aren't already bound. The actions — `bind`, `setPolicy`, `unbind`, -and `remove` — are workspace-admin-gated. +Bind and unbind from a profile's **Workspace bindings** section in Mission +Control. The workspace selector only offers workspaces where the caller is an +owner or admin, and the server repeats that authorization check before every +write. Workspace settings at **`/w/[slug]/settings/agents`** retain only +workspace policy: capacity, capability overrides, routing eligibility, +engagement mode, and approval gates. ::: tip Unbind vs Delete **Unbind** archives the binding (reversible) — runs, chats, and history are -preserved, and re-binding the same profile reuses the archived row. -**Delete** (the trash action beside Unbind) is a _smart remove_: it -hard-deletes a genuinely unused agent (no runs, comments, keys, or -assignments) and otherwise archives it — so a deletion can never cascade -away an agent's run history. +preserved, and re-binding the same profile reuses the archived row. Workspace +admins can unbind but cannot delete the global identity. ::: ## Tier 3 — Instance policy @@ -75,19 +73,22 @@ Two governance levers belong to the instance admin (see - **`disabledAt`** — a force-disable. A disabled profile (and all its bindings) refuses dispatch and chat. Distinct from `archivedAt` (deletion). -- **Remove** — Instance Admin → Agents has a **Remove** action - (`agents.profiles.remove`) beside Enable/Disable: it deletes a profile no - workspace has bound, and archives one that still has bindings (so those - agents aren't orphaned). Archived profiles drop out of the admin list and - the bind-catalog. +- **Approve or reject** — pending profile requests remain instance-governed. + +Instance Admin → Agent governance intentionally does not duplicate routine +profile management. Each row links to Mission Control, where an instance admin +gets the safe **Remove profile** action (`agents.profiles.remove`): it deletes a +profile no workspace has bound, and archives one that still has bindings so +agents and history are not orphaned. Profile owners who are not instance admins +get **Archive profile** instead. ## Requesting a profile Defining a _new_ profile from scratch is instance-admin-only — but any member can **request** one: -1. A member calls `agentProfile.request` (the **Request a profile** - action on the workspace agents page) with a `profileKey`, name, and +1. A member calls `agentProfile.request` (the **Request profile** action in + Mission Control → Agents) with a `profileKey`, name, and base capabilities. This creates a **pending** profile (`requestedById` = the requester, `approvedAt` = null). 2. A pending profile is **not bindable** — it's hidden from every @@ -106,7 +107,7 @@ runtime, while each workspace binding may have any number of linked `ApiKey` credentials for Codex, Claude, Hermes, or other trusted MCP clients. Create, inspect, rotate, revoke, and remove those credentials at -**`/w/[slug]/settings/access`**. Agent Studio aggregates the linked clients by +**`/w/[slug]/settings/access`**. Mission Control aggregates the linked clients by binding and deep-links into Agent access with the correct workspace agent preselected. The former `/settings/clients` and `/w/[slug]/settings/clients` inventory routes redirect to Agent access. diff --git a/docs/agents/runtimes.md b/docs/agents/runtimes.md index a51eb1ca..bd8f2638 100644 --- a/docs/agents/runtimes.md +++ b/docs/agents/runtimes.md @@ -6,7 +6,7 @@ mode) and `runtimeMode` describes _how_ the agent stays online, `Runtime` describes _where the work physically happens_. ::: info Runtime versus MCP client -An Agent Studio profile selects **zero or one primary execution runtime**. +A Mission Control agent profile selects **zero or one primary execution runtime**. Codex, Claude, Hermes, and custom MCP clients connect with separate linked credentials in the workspace's **Agent access** page; a binding can have many of those clients. Adding an MCP client never changes the profile's runtime. @@ -60,7 +60,7 @@ columns. `LOCAL_DAEMON` rows. The detail page also shows whether the runtime declares `terminal`, `filesystem`, and `git` access, plus the latest sanitized runtime version/environment metadata when the host reports it. -- **Agent Studio profile detail** — readiness summary plus an editable primary +- **Mission Control agent profile detail** — readiness summary plus an editable primary Runtime assignment that synchronizes to active workspace bindings. - **Mission Control agents tab** — compact `RuntimeChip` next to the runtime-mode pill. diff --git a/docs/guide/instance-admin.md b/docs/guide/instance-admin.md index ae05f8e6..74878897 100644 --- a/docs/guide/instance-admin.md +++ b/docs/guide/instance-admin.md @@ -1,7 +1,7 @@ # Instance Admin `/admin` is the instance-operator surface — the controls that span -*every* tenant on a Forge instance, rather than one workspace. It renders +_every_ tenant on a Forge instance, rather than one workspace. It renders in its own graphite shell (distinct from the warm-paper workspace shell) to make it obvious you've stepped up a level. @@ -10,10 +10,10 @@ to make it obvious you've stepped up a level. The `/admin` area is gated on `User.instanceRole === INSTANCE_ADMIN`. There are exactly two instance roles: -| `InstanceRole` | Grants | -|---|---| +| `InstanceRole` | Grants | +| ---------------- | ------------------------------------------------------ | | `INSTANCE_ADMIN` | The `/admin` shell + every `instanceAdmin.*` procedure | -| `MEMBER` | Default; no instance-level access | +| `MEMBER` | Default; no instance-level access | This is **orthogonal to workspace roles**. Being `OWNER` of a workspace doesn't make you an instance admin, and an instance admin isn't @@ -79,10 +79,16 @@ instance admin can approve or reject (`agentProfile.listPending` → `instanceShared` or force-disable one. See [Agent profiles & bindings](/agents/profiles-and-bindings.html#requesting-a-profile). +Routine profile management does not live in the graphite admin shell. Each +governance row links to **Mission Control → Agents**, where authorized instance +admins can edit and safely remove a profile. Removal permanently deletes only +an unreferenced profile; otherwise it archives the definition and preserves +workspace bindings and history. + ## Where to next -- [Mission Control](/guide/mission-control.html) — the cross-workspace - *user* home (read-only), separate from this operator surface. +- [Mission Control](/guide/mission-control.html) — cross-workspace operations + plus the agent fleet control plane, separate from instance governance. - [Agent profiles & bindings](/agents/profiles-and-bindings.html) — the approval + instance-sharing flow admins govern. - [Runtimes](/agents/runtimes.html) — the compute hosts the instance diff --git a/docs/guide/mission-control.md b/docs/guide/mission-control.md index f33be89e..bd33386e 100644 --- a/docs/guide/mission-control.md +++ b/docs/guide/mission-control.md @@ -1,10 +1,9 @@ # Mission Control -Mission Control is the cross-workspace home — the screen at `/` when you -sign in. Where a workspace shows you one tenant's world, Mission Control -shows you *all of them at once*: your assigned work, your agents, your -runtimes, and live activity, each row stamped with the workspace it -belongs to. +Mission Control is Forge's cross-workspace operating layer. The overview at +`/` shows every workspace at once; **Agents** at `/agents` is the global fleet +control plane for defining identities, binding them into workspaces, and +monitoring live use. ::: tip Chord g h brings you home to Mission Control from @@ -20,15 +19,17 @@ three global destinations, the workspace switcher, and links into account-level settings: - **Mission Control** (`/`) — this page. +- **Agents** (`/agents`) — global profiles, execution connections, workspace + bindings, recent runs, and safe archive/removal controls. - **Inbox** (`/inbox`) — your mentions and assignments, aggregated across workspaces. - **Activity** (`/activity`) — the live cross-workspace run + event feed. -Everything in the concourse is **read-only**. The cards show you what's -happening and link you *into* the right workspace; the actual -writes — transitioning an issue, replying to a comment, dispatching an -agent — happen once you've clicked through into that workspace's shell. -This is deliberate: a single mutation always has one unambiguous tenant. +The overview, Inbox, and Activity remain **read-only across workspaces**. The +Agents destination is the deliberate exception and carries a **Global control +plane** scope label. Its binding writes always name one workspace explicitly +and require an owner/admin membership there; workspace policy edits still +happen inside that workspace's settings. ## The cards @@ -39,12 +40,12 @@ client-side from the `global.*` tRPC routers. Four tiles across the top: -| Tile | Shows | -|---|---| -| **Assigned to me** | Count of issues assigned to you, across every workspace | -| **Open issues** | Open-issue count summed across all your workspaces | +| Tile | Shows | +| --------------------- | -------------------------------------------------------- | +| **Assigned to me** | Count of issues assigned to you, across every workspace | +| **Open issues** | Open-issue count summed across all your workspaces | | **Agent runs · live** | Active `AgentRun` count, plus how many agents are online | -| **Runtimes online** | `online / total` runtimes you've registered | +| **Runtimes online** | `online / total` runtimes you've registered | ### My work @@ -62,7 +63,7 @@ rollup (`open · live · members`). Click to enter the workspace. The Your agent **profiles** (global definitions you own), each with an online pip, `@profileKey`, the workspaces it's bound into, and its -provider. Click through to the profile detail under `/settings/agents`. +provider. Click through to the profile detail under `/agents`. See [Agent profiles & bindings](/agents/profiles-and-bindings.html) for the profile → binding model. diff --git a/src/app/(app)/admin/agents/page.tsx b/src/app/(app)/admin/agents/page.tsx index 9621cb2f..09c445c8 100644 --- a/src/app/(app)/admin/agents/page.tsx +++ b/src/app/(app)/admin/agents/page.tsx @@ -9,10 +9,10 @@ export default function AdminAgentsPage() { return ( diff --git a/src/app/(app)/agents/[id]/page.tsx b/src/app/(app)/agents/[id]/page.tsx new file mode 100644 index 00000000..44659e07 --- /dev/null +++ b/src/app/(app)/agents/[id]/page.tsx @@ -0,0 +1,31 @@ +import { redirect } from "next/navigation"; +import { GlobalShell } from "@/components/global-shell/global-shell"; +import { getGlobalShellData } from "@/components/global-shell/data"; +import { AgentDetailContent } from "@/app/(app)/settings/agents/[id]/agent-detail-content"; + +/** Mission Control profile detail and binding control surface. */ +export default async function AgentFleetDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const [data, { id }] = await Promise.all([getGlobalShellData(), params]); + if (!data) redirect("/signin"); + + return ( + + + + ); +} diff --git a/src/app/(app)/agents/page.tsx b/src/app/(app)/agents/page.tsx new file mode 100644 index 00000000..d930863f --- /dev/null +++ b/src/app/(app)/agents/page.tsx @@ -0,0 +1,34 @@ +import { redirect } from "next/navigation"; +import { GlobalShell } from "@/components/global-shell/global-shell"; +import { getGlobalShellData } from "@/components/global-shell/data"; +import { AgentsContent } from "@/app/(app)/settings/agents/agents-content"; + +/** Mission Control fleet: define, bind, and operate global agent identities. */ +export default async function AgentFleetPage({ + searchParams, +}: { + searchParams: Promise<{ workspace?: string }>; +}) { + const [data, query] = await Promise.all([getGlobalShellData(), searchParams]); + if (!data) redirect("/signin"); + + return ( + + + + ); +} diff --git a/src/app/(app)/settings/access/page.tsx b/src/app/(app)/settings/access/page.tsx index df7512a7..cd1ee3e5 100644 --- a/src/app/(app)/settings/access/page.tsx +++ b/src/app/(app)/settings/access/page.tsx @@ -1322,7 +1322,8 @@ function Intro({ baseUrl }: { baseUrl: string }) {
This page is the credential source of truth: create, inspect, rotate, revoke, and delete - every MCP client here. Primary execution runtimes stay with the identity in Agent Studio. + every MCP client here. Primary execution runtimes stay with the identity in Mission Control + → Agents.
); diff --git a/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx b/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx index c1da596b..4f4277f7 100644 --- a/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx +++ b/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx @@ -2,13 +2,27 @@ import { useState } from "react"; import Link from "next/link"; -import { ArrowLeft, CheckCircle2, KeyRound, Link2, Server, TriangleAlert } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { + Archive, + ArrowLeft, + CheckCircle2, + KeyRound, + Link2, + Plus, + Server, + Trash2, + TriangleAlert, + X, +} from "lucide-react"; import { AgentProvider, RunEngine } from "@prisma/client"; import { toast } from "sonner"; import { Topbar } from "@/components/topbar"; import { Spinner, EmptyState, Section } from "@/components/ui"; import { Button } from "@/components/ui/button"; +import { Combobox } from "@/components/ui/combobox"; import { Input } from "@/components/ui/input"; +import { Confirm } from "@/components/ui/modal"; import { trpc } from "@/lib/trpc"; import { relativeTime } from "@/lib/utils"; import { workspaceChipColor } from "@/components/global-shell/global-shell"; @@ -114,7 +128,24 @@ function ReadinessCard({ ); } -export function AgentDetailContent({ id }: { id: string }) { +type ProfileWorkspace = { + id: string; + slug: string; + name: string; + key: string; + role?: "OWNER" | "ADMIN" | "MEMBER" | "GUEST"; +}; + +export function AgentDetailContent({ + id, + isInstanceAdmin, + workspaces, +}: { + id: string; + isInstanceAdmin: boolean; + workspaces: ProfileWorkspace[]; +}) { + const router = useRouter(); const utils = trpc.useUtils(); const [promptDraft, setPromptDraft] = useState(null); const [definitionDraft, setDefinitionDraft] = useState<{ @@ -126,6 +157,13 @@ export function AgentDetailContent({ id }: { id: string }) { runtimeId: string; capabilities: string; } | null>(null); + const [bindWorkspaceId, setBindWorkspaceId] = useState(""); + const [unbindTarget, setUnbindTarget] = useState<{ + agentId: string; + workspaceId: string; + workspaceName: string; + } | null>(null); + const [dangerOpen, setDangerOpen] = useState(false); const { data: a, isLoading, error } = trpc.agents.profiles.get.useQuery({ id }); const { data: runtimes } = trpc.global.runtimes.useQuery(); const updateProfile = trpc.agents.profiles.update.useMutation({ @@ -138,6 +176,46 @@ export function AgentDetailContent({ id }: { id: string }) { }, onError: (err) => toast.error(err.message), }); + const bindProfile = trpc.agents.bindings.bindFromMissionControl.useMutation({ + onSuccess: async () => { + toast.success("Agent bound to the workspace."); + setBindWorkspaceId(""); + await Promise.all([ + utils.agents.profiles.get.invalidate({ id }), + utils.agents.profiles.list.invalidate(), + ]); + }, + onError: (err) => toast.error(err.message), + }); + const unbindProfile = trpc.agents.bindings.unbindFromMissionControl.useMutation({ + onSuccess: async () => { + toast.success("Agent unbound. Runs, chats, and history are preserved."); + setUnbindTarget(null); + await Promise.all([ + utils.agents.profiles.get.invalidate({ id }), + utils.agents.profiles.list.invalidate(), + ]); + }, + onError: (err) => toast.error(err.message), + }); + const archiveProfile = trpc.agents.profiles.archive.useMutation({ + onSuccess: () => { + toast.success("Agent profile archived. Workspace history is preserved."); + router.push("/agents"); + }, + onError: (err) => toast.error(err.message), + }); + const removeProfile = trpc.agents.profiles.remove.useMutation({ + onSuccess: (result) => { + toast.success( + result.action === "deleted" + ? `Deleted ${result.name}.` + : `Archived ${result.name}; ${result.boundAgents} workspace binding${result.boundAgents === 1 ? "" : "s"} still reference it.`, + ); + router.push("/agents"); + }, + onError: (err) => toast.error(err.message), + }); if (isLoading) { return ( @@ -161,7 +239,7 @@ export function AgentDetailContent({ id }: { id: string }) { description={error?.message ?? "This profile doesn't exist or you don't have access."} action={ Back to agents @@ -192,6 +270,12 @@ export function AgentDetailContent({ id }: { id: string }) { ); const executionReady = !!a.runtime; const profile = a; + const boundWorkspaceIds = new Set(a.bindings.map((binding) => binding.workspace.id)); + const bindableWorkspaces = workspaces.filter( + (workspace) => + (workspace.role === "OWNER" || workspace.role === "ADMIN") && + !boundWorkspaceIds.has(workspace.id), + ); function beginDefinitionEdit() { setDefinitionDraft({ @@ -248,11 +332,14 @@ export function AgentDetailContent({ id }: { id: string }) {
All agents + + Global profile +
@@ -523,13 +610,15 @@ export function AgentDetailContent({ id }: { id: string }) { )}
- - - Add MCP client - + {binding.canManage && ( + + + Add MCP client + + )} ); }) @@ -619,27 +708,58 @@ export function AgentDetailContent({ id }: { id: string }) { {/* Workspace bindings */}
0 ? ( +
+ setBindWorkspaceId(value ?? "")} + options={bindableWorkspaces.map((workspace) => ({ + value: workspace.id, + label: workspace.name, + secondary: workspace.key, + }))} + placeholder="Choose workspace…" + className="h-8 max-w-52 text-xs" + popoverClassName="min-w-56" + /> + +
+ ) : undefined + } >
-
+
Workspace Status Max concurrent Capabilities Auto-dispatch + Actions
{a.bindings.length === 0 ? ( ) : ( a.bindings.map((b) => (
{b.status.toLowerCase()} - + {b.maxConcurrent} @@ -684,13 +804,35 @@ export function AgentDetailContent({ id }: { id: string }) { )) )} - + {b.autoDispatchEligible ? ( on ) : ( off )} + + + + + {b.canManage && ( + + )} +
)) )} @@ -728,8 +870,80 @@ export function AgentDetailContent({ id }: { id: string }) { )}
+ + {a.canEdit && ( +
+
+
+
+ {isInstanceAdmin ? "Remove global profile" : "Archive global profile"} +
+

+ {isInstanceAdmin + ? "Unused profiles are deleted permanently. Profiles with workspace bindings are archived so runs, chats, and history remain intact." + : "Archiving hides the profile from Mission Control and binding catalogs while preserving workspace history."} +

+
+ +
+
+ )} + + !open && setUnbindTarget(null)} + variant="destructive" + title={`Unbind from ${unbindTarget?.workspaceName}?`} + description="The agent stops being available in this workspace. Runs, chats, and history are preserved, and re-binding restores the same binding." + primaryLabel="Unbind" + loading={unbindProfile.isPending} + onConfirm={async () => { + if (!unbindTarget) return; + await unbindProfile.mutateAsync({ + workspaceId: unbindTarget.workspaceId, + agentId: unbindTarget.agentId, + }); + }} + /> + +