From a9e808375a0976492d04ee65692371c8ef6f6c01 Mon Sep 17 00:00:00 2001
From: klopez4212 <klopez4212@gmail.com>
Date: Tue, 23 Jun 2026 16:31:31 +0100
Subject: [PATCH 1/6] Simplify agents and teams cards

---
 .../features/agents/ui/AgentIdentityCard.tsx  |  69 ++
 .../features/agents/ui/CreateIdentityCard.tsx |  37 +
 .../agents/ui/IdentityInitialsAvatar.tsx      |  59 ++
 .../features/agents/ui/TeamIdentityCard.tsx   | 180 ++++
 .../src/features/agents/ui/TeamsSection.tsx   | 268 +++---
 .../agents/ui/UnifiedAgentsSection.tsx        | 879 ++++++++++++------
 .../src/shared/ui/identity-card-skeleton.tsx  |  44 +
 7 files changed, 1089 insertions(+), 447 deletions(-)
 create mode 100644 desktop/src/features/agents/ui/AgentIdentityCard.tsx
 create mode 100644 desktop/src/features/agents/ui/CreateIdentityCard.tsx
 create mode 100644 desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
 create mode 100644 desktop/src/features/agents/ui/TeamIdentityCard.tsx
 create mode 100644 desktop/src/shared/ui/identity-card-skeleton.tsx

diff --git a/desktop/src/features/agents/ui/AgentIdentityCard.tsx b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
new file mode 100644
index 0000000000..a66fb9cd7e
--- /dev/null
+++ b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
@@ -0,0 +1,69 @@
+import type { ReactNode } from "react";
+
+import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
+import { cn } from "@/shared/lib/cn";
+import { IdentityInitialsAvatar } from "./IdentityInitialsAvatar";
+
+type AgentIdentityCardProps = {
+  actions?: ReactNode;
+  ariaLabel: string;
+  avatarUrl?: string | null;
+  dataTestId: string;
+  label: string;
+  modelLabel: string;
+  onClick: () => void;
+};
+
+export function AgentIdentityCard({
+  actions,
+  ariaLabel,
+  avatarUrl,
+  dataTestId,
+  label,
+  modelLabel,
+  onClick,
+}: AgentIdentityCardProps) {
+  const trimmedAvatarUrl = avatarUrl?.trim() || null;
+
+  return (
+    <div
+      className={cn(
+        "group relative aspect-[4/5] w-full min-w-0 overflow-hidden rounded-xl border border-border/70 bg-muted/50 text-left shadow-xs transition-colors hover:border-border hover:bg-muted/65",
+      )}
+      data-testid={dataTestId}
+    >
+      <button
+        aria-label={ariaLabel}
+        className="flex h-full w-full min-w-0 flex-col items-center justify-center gap-5 px-4 pb-12 text-center focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
+        onClick={onClick}
+        type="button"
+      >
+        <div className="flex h-24 w-24 items-center justify-center">
+          {trimmedAvatarUrl ? (
+            <ProfileAvatar
+              avatarUrl={trimmedAvatarUrl}
+              className="h-full w-full border-[3px] border-background bg-muted shadow-sm"
+              iconClassName="h-8 w-8"
+              label={label}
+            />
+          ) : (
+            <IdentityInitialsAvatar label={label} size={96} />
+          )}
+        </div>
+      </button>
+
+      {actions ? (
+        <div className="absolute top-3 right-3 z-40">{actions}</div>
+      ) : null}
+
+      <div className="absolute right-3 bottom-3 left-3 z-30 flex min-w-0 flex-col gap-0.5 text-left text-sm leading-5">
+        <span className="min-w-0 truncate font-semibold text-foreground tracking-normal">
+          {label}
+        </span>
+        <span className="min-w-0 truncate font-normal text-secondary-foreground/75">
+          {modelLabel}
+        </span>
+      </div>
+    </div>
+  );
+}
diff --git a/desktop/src/features/agents/ui/CreateIdentityCard.tsx b/desktop/src/features/agents/ui/CreateIdentityCard.tsx
new file mode 100644
index 0000000000..3efd91e19f
--- /dev/null
+++ b/desktop/src/features/agents/ui/CreateIdentityCard.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import { Plus } from "lucide-react";
+
+import { cn } from "@/shared/lib/cn";
+
+type CreateIdentityCardProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
+  ariaLabel: string;
+  dataTestId: string;
+  label: string;
+};
+
+export const CreateIdentityCard = React.forwardRef<
+  HTMLButtonElement,
+  CreateIdentityCardProps
+>(function CreateIdentityCard(
+  { ariaLabel, className, dataTestId, label, ...buttonProps },
+  ref,
+) {
+  return (
+    <button
+      aria-label={ariaLabel}
+      className={cn(
+        "group relative flex aspect-[4/5] w-full min-w-0 items-center justify-center overflow-hidden rounded-xl border border-dashed border-border/80 bg-transparent text-muted-foreground shadow-xs transition-colors hover:border-border hover:bg-muted/70 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
+        className,
+      )}
+      data-testid={dataTestId}
+      ref={ref}
+      type="button"
+      {...buttonProps}
+    >
+      <span className="flex flex-col items-center justify-center gap-2 text-center">
+        <Plus className="h-7 w-7 transition-colors" />
+        <span className="text-sm font-medium leading-5">{label}</span>
+      </span>
+    </button>
+  );
+});
diff --git a/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx b/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
new file mode 100644
index 0000000000..71fa310656
--- /dev/null
+++ b/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
@@ -0,0 +1,59 @@
+import { UserRound } from "lucide-react";
+
+import { getInitials } from "@/shared/lib/initials";
+import { cn } from "@/shared/lib/cn";
+
+const IDENTITY_INITIAL_AVATAR_CLASS_NAMES = [
+  "bg-muted text-foreground",
+  "bg-secondary text-secondary-foreground",
+  "bg-accent text-accent-foreground",
+  "bg-card text-card-foreground",
+  "bg-popover text-popover-foreground",
+  "bg-background text-foreground",
+] as const;
+
+type IdentityInitialsAvatarProps = {
+  className?: string;
+  colorIndex?: number;
+  colorSeed?: string;
+  label: string;
+  size: number;
+};
+
+export function IdentityInitialsAvatar({
+  className,
+  colorIndex,
+  colorSeed,
+  label,
+  size,
+}: IdentityInitialsAvatarProps) {
+  const initials = getInitials(label);
+  const seed = colorSeed ?? (label || "agent");
+  const paletteIndex = colorIndex ?? getStableColorIndex(seed);
+  const colorClassName =
+    IDENTITY_INITIAL_AVATAR_CLASS_NAMES[
+      paletteIndex % IDENTITY_INITIAL_AVATAR_CLASS_NAMES.length
+    ];
+  const fontSize = Math.round(Math.min(40, Math.max(22, size * 0.28)));
+
+  return (
+    <span
+      className={cn(
+        "flex h-full w-full items-center justify-center rounded-full border-[3px] border-background font-semibold shadow-sm",
+        colorClassName,
+        className,
+      )}
+      style={{ fontSize }}
+    >
+      {initials.length > 0 ? initials : <UserRound className="h-8 w-8" />}
+    </span>
+  );
+}
+
+function getStableColorIndex(seed: string) {
+  let hash = 0;
+  for (let index = 0; index < seed.length; index += 1) {
+    hash = (hash * 31 + seed.charCodeAt(index)) >>> 0;
+  }
+  return hash;
+}
diff --git a/desktop/src/features/agents/ui/TeamIdentityCard.tsx b/desktop/src/features/agents/ui/TeamIdentityCard.tsx
new file mode 100644
index 0000000000..17d129e6f8
--- /dev/null
+++ b/desktop/src/features/agents/ui/TeamIdentityCard.tsx
@@ -0,0 +1,180 @@
+import type { ReactNode } from "react";
+import { Link, Users } from "lucide-react";
+
+import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
+import type { AgentPersona } from "@/shared/api/types";
+import { Card } from "@/shared/ui/card";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
+import { IdentityInitialsAvatar } from "./IdentityInitialsAvatar";
+
+type TeamIdentityCardProps = {
+  actions: ReactNode;
+  children?: ReactNode;
+  dataTestId: string;
+  description?: string | null;
+  isSymlink?: boolean;
+  memberCount: number;
+  personas: AgentPersona[];
+  sourceDir?: string | null;
+  symlinkTarget?: string | null;
+  teamId: string;
+  teamName: string;
+  version?: string | null;
+};
+
+const MAX_VISIBLE_MEMBER_AVATARS = 4;
+
+export function TeamIdentityCard({
+  actions,
+  children,
+  dataTestId,
+  isSymlink = false,
+  memberCount,
+  personas,
+  sourceDir,
+  symlinkTarget,
+  teamName,
+  version,
+}: TeamIdentityCardProps) {
+  const footerModelLabel = getTeamFooterModelLabel(personas);
+
+  return (
+    <Card
+      className="min-w-0 overflow-hidden p-0 transition-colors hover:border-border hover:bg-muted/65"
+      data-testid={dataTestId}
+    >
+      <div className="relative aspect-[4/5] min-w-0 overflow-hidden bg-muted/50">
+        <div className="absolute top-3 left-3 z-30 flex max-w-[calc(100%-4rem)] flex-wrap items-center gap-1.5">
+          {isSymlink ? (
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <span className="flex h-6 w-6 items-center justify-center rounded-full border border-border/65 bg-background/90 text-muted-foreground shadow-xs">
+                  <Link className="h-3.5 w-3.5" />
+                </span>
+              </TooltipTrigger>
+              <TooltipContent side="bottom" className="max-w-xs">
+                <p>Linked from {symlinkTarget ?? sourceDir}</p>
+              </TooltipContent>
+            </Tooltip>
+          ) : null}
+          {version ? (
+            <span className="rounded-full border border-border/65 bg-background/90 px-2 py-1 text-2xs font-medium leading-none text-muted-foreground shadow-xs">
+              v{version}
+            </span>
+          ) : null}
+        </div>
+
+        <div className="absolute top-3 right-3 z-40">{actions}</div>
+
+        <TeamAvatarRow
+          memberCount={memberCount}
+          personas={personas}
+          teamName={teamName}
+        />
+
+        <div className="absolute right-3 bottom-3 left-3 z-30 flex min-w-0 flex-col gap-0.5 text-left text-sm leading-5">
+          <span className="min-w-0 truncate font-semibold tracking-normal text-foreground">
+            {teamName}
+          </span>
+          <span className="min-w-0 truncate font-normal text-secondary-foreground/75">
+            {footerModelLabel}
+          </span>
+        </div>
+      </div>
+      {children}
+    </Card>
+  );
+}
+
+function TeamAvatarRow({
+  memberCount,
+  personas,
+  teamName,
+}: {
+  memberCount: number;
+  personas: AgentPersona[];
+  teamName: string;
+}) {
+  const visiblePersonas = personas.slice(0, MAX_VISIBLE_MEMBER_AVATARS);
+  const overflowCount = Math.max(0, memberCount - visiblePersonas.length);
+
+  if (visiblePersonas.length === 0 && overflowCount === 0) {
+    return (
+      <div className="absolute inset-x-4 top-0 bottom-12 flex items-center justify-center">
+        <div className="flex h-24 w-24 items-center justify-center rounded-full border border-border/65 bg-background/80 text-muted-foreground shadow-xs">
+          <Users className="h-9 w-9" />
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="absolute inset-x-0 top-0 bottom-12 flex items-center justify-center">
+      <div
+        aria-label={`${teamName} member avatars`}
+        className="flex max-w-full items-center justify-center gap-2 px-4"
+        role="img"
+      >
+        {visiblePersonas.map((persona, index) => (
+          <TeamAvatarItem index={index} key={persona.id} persona={persona} />
+        ))}
+        {overflowCount > 0 ? (
+          <span className="flex h-14 w-14 items-center justify-center rounded-full border-[3px] border-background bg-card text-sm font-semibold text-muted-foreground shadow-sm">
+            +{overflowCount}
+          </span>
+        ) : null}
+      </div>
+    </div>
+  );
+}
+
+function TeamAvatarItem({
+  index,
+  persona,
+}: {
+  index: number;
+  persona: AgentPersona;
+}) {
+  const avatarUrl = persona.avatarUrl?.trim() ?? null;
+
+  return (
+    <div className="h-14 w-14" data-team-member-avatar="avatar">
+      {avatarUrl ? (
+        <ProfileAvatar
+          avatarUrl={avatarUrl}
+          className="h-full w-full border-[3px] border-background bg-muted shadow-sm"
+          iconClassName="h-6 w-6"
+          label={persona.displayName}
+          testId={`team-member-avatar-${persona.id}`}
+        />
+      ) : (
+        <IdentityInitialsAvatar
+          colorIndex={index}
+          label={persona.displayName}
+          size={56}
+        />
+      )}
+    </div>
+  );
+}
+
+function getTeamFooterModelLabel(personas: AgentPersona[]) {
+  const modelLabels = personas
+    .map((persona) => formatFooterModelLabel(persona.model))
+    .filter((model): model is string => Boolean(model));
+
+  if (modelLabels.length === 0) return "Auto";
+
+  const uniqueModels = new Map(
+    modelLabels.map((model) => [model.toLowerCase(), model]),
+  );
+
+  return uniqueModels.size === 1
+    ? (uniqueModels.values().next().value ?? "Auto")
+    : "Mixed models";
+}
+
+function formatFooterModelLabel(model: string | null | undefined) {
+  const trimmed = model?.trim();
+  return trimmed && trimmed.length > 0 ? trimmed : "Auto";
+}
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index 4d0e4b66d5..daf4f8151b 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -4,17 +4,12 @@ import {
   Ellipsis,
   FolderOpen,
   FolderSync,
-  Info,
-  Link,
   Pencil,
   Rocket,
   Trash2,
-  Upload,
-  Users,
 } from "lucide-react";
 
 import { resolveTeamPersonas } from "@/features/agents/lib/teamPersonas";
-import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
 import type { AgentPersona, AgentTeam } from "@/shared/api/types";
 import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
 import {
@@ -24,12 +19,12 @@ import {
   DropdownMenuSeparator,
   DropdownMenuTrigger,
 } from "@/shared/ui/dropdown-menu";
-import { Card } from "@/shared/ui/card";
-import { Skeleton } from "@/shared/ui/skeleton";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
-import { CreateNewButton } from "./CreateNewButton";
+import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
+import { CreateIdentityCard } from "./CreateIdentityCard";
+import { TeamIdentityCard } from "./TeamIdentityCard";
 
-const MAX_VISIBLE_AVATARS = 4;
+const TEAM_CARD_COLUMN_CLASS = "w-full";
+const TEAM_CARD_GRID_CLASS = `${TEAM_CARD_COLUMN_CLASS} grid grid-cols-[repeat(auto-fill,minmax(220px,240px))] justify-start gap-3`;
 
 type TeamsSectionProps = {
   teams: AgentTeam[];
@@ -43,10 +38,10 @@ type TeamsSectionProps = {
   onExport: (team: AgentTeam) => void;
   onDelete: (team: AgentTeam) => void;
   onAddToChannel: (team: AgentTeam) => void;
-  onImportFile: (fileBytes: number[], fileName: string) => void;
-  onInstallFromDirectory: () => void;
   onSync: (team: AgentTeam) => void;
   onRevealInFinder: (team: AgentTeam) => void;
+  onImportFile: (fileBytes: number[], fileName: string) => void;
+  onInstallFromDirectory?: () => void;
 };
 
 export function TeamsSection({
@@ -61,10 +56,10 @@ export function TeamsSection({
   onExport,
   onDelete,
   onAddToChannel,
-  onImportFile,
-  onInstallFromDirectory,
   onSync,
   onRevealInFinder,
+  onImportFile,
+  onInstallFromDirectory,
 }: TeamsSectionProps) {
   const {
     fileInputRef,
@@ -83,144 +78,64 @@ export function TeamsSection({
       {isDragOver ? (
         <div className="pointer-events-none absolute -inset-1 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary/50 bg-background/80 backdrop-blur-sm">
           <p className="text-sm font-medium text-primary">
-            Drop .team.json to import
+            Drop .team.json or .zip to import
           </p>
         </div>
       ) : null}
+      <input
+        accept=".json,.zip"
+        className="hidden"
+        onChange={handleFileChange}
+        ref={fileInputRef}
+        type="file"
+      />
 
-      <div className="flex items-center justify-between gap-3">
+      <div
+        className={`${TEAM_CARD_COLUMN_CLASS} flex items-center justify-between gap-3`}
+      >
         <div>
           <h3 className="text-sm font-semibold tracking-tight">My teams</h3>
-          <p className="text-sm text-muted-foreground">
+          <p className="text-sm text-secondary-foreground/75">
             Saved groups from My Agents that you can add to a channel together.
           </p>
         </div>
-        <input
-          accept=".json,.zip"
-          className="hidden"
-          onChange={handleFileChange}
-          ref={fileInputRef}
-          type="file"
-        />
-        <div className="flex items-center gap-2">
-          <button
-            className="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
-            onClick={onInstallFromDirectory}
-            type="button"
-          >
-            <FolderOpen className="h-4 w-4" />
-            Install from directory
-          </button>
-          <CreateNewButton
-            ariaLabel="Create team"
-            label="Team"
-            onClick={onCreate}
-          />
-        </div>
       </div>
 
       {isLoading ? (
-        <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
-          {["first", "second", "third"].map((key) => (
-            <Card className="p-3" key={key}>
-              <div className="flex items-center gap-2.5">
-                <Skeleton className="h-8 w-8 rounded-lg" />
-                <div className="space-y-2">
-                  <Skeleton className="h-4 w-32" />
-                  <Skeleton className="h-3 w-20 rounded-full" />
-                </div>
-              </div>
-            </Card>
-          ))}
+        <div className={TEAM_CARD_GRID_CLASS}>
+          <IdentityCardSkeleton
+            footerSubtitleWidthClass="w-14"
+            footerTitleWidthClass="w-24"
+            showAction
+          />
+          <IdentityCardSkeleton
+            footerSubtitleWidthClass="w-24"
+            footerTitleWidthClass="w-32"
+            showAction
+          />
+          <IdentityCardSkeleton
+            footerSubtitleWidthClass="w-20"
+            footerTitleWidthClass="w-28"
+            showAction
+          />
         </div>
       ) : null}
 
-      {!isLoading && teams.length > 0 ? (
-        <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
+      {!isLoading ? (
+        <div className={TEAM_CARD_GRID_CLASS}>
           {teams.map((team) => {
             const resolution = resolveTeamPersonas(team, personas);
-            const visible = resolution.resolvedPersonas.slice(
-              0,
-              MAX_VISIBLE_AVATARS,
-            );
-            const overflow =
-              resolution.resolvedPersonas.length - visible.length;
             const missingPersonaCount = resolution.missingPersonaCount;
             const hasMissingPersonas = resolution.hasMissingPersonas;
 
             return (
-              <Card className="p-3" key={team.id}>
-                <div className="flex items-start justify-between gap-2.5">
-                  <div className="min-w-0 flex-1">
-                    <div className="flex items-center gap-2">
-                      <Users className="h-4 w-4 shrink-0 text-muted-foreground" />
-                      <p className="truncate text-sm font-semibold tracking-tight">
-                        {team.name}
-                      </p>
-                      {team.isSymlink ? (
-                        <Tooltip>
-                          <TooltipTrigger asChild>
-                            <span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
-                              <Link className="h-4 w-4" />
-                            </span>
-                          </TooltipTrigger>
-                          <TooltipContent side="bottom" className="max-w-xs">
-                            <p>
-                              Linked from {team.symlinkTarget ?? team.sourceDir}
-                            </p>
-                          </TooltipContent>
-                        </Tooltip>
-                      ) : null}
-                      {team.version ? (
-                        <span className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-2xs font-medium text-muted-foreground">
-                          v{team.version}
-                        </span>
-                      ) : null}
-                      {team.description ? (
-                        <Tooltip>
-                          <TooltipTrigger asChild>
-                            <button
-                              aria-label="View description"
-                              className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
-                              type="button"
-                            >
-                              <Info className="h-4 w-4" />
-                            </button>
-                          </TooltipTrigger>
-                          <TooltipContent side="bottom" className="max-w-xs">
-                            <p>{team.description}</p>
-                          </TooltipContent>
-                        </Tooltip>
-                      ) : null}
-                    </div>
-
-                    <div className="mt-2 flex items-center gap-2">
-                      <div className="flex -space-x-1.5">
-                        {visible.map((persona) => (
-                          <ProfileAvatar
-                            avatarUrl={persona.avatarUrl}
-                            className="h-6 w-6 border-2 border-card text-2xs"
-                            key={persona.id}
-                            label={persona.displayName}
-                          />
-                        ))}
-                        {overflow > 0 ? (
-                          <span className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-card bg-muted text-2xs font-medium text-muted-foreground">
-                            +{overflow}
-                          </span>
-                        ) : null}
-                      </div>
-                      <span className="text-xs text-muted-foreground">
-                        {team.personaIds.length}{" "}
-                        {team.personaIds.length === 1 ? "persona" : "personas"}
-                      </span>
-                    </div>
-                  </div>
-
+              <TeamIdentityCard
+                actions={
                   <DropdownMenu modal={false}>
                     <DropdownMenuTrigger asChild>
                       <button
-                        className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
+                        aria-label={`${team.name} team actions`}
+                        className="flex h-7 w-7 items-center justify-center rounded-md bg-transparent text-muted-foreground/80 transition-colors hover:bg-background/85 hover:text-foreground data-[state=open]:bg-background/90 data-[state=open]:text-foreground"
                         type="button"
                       >
                         <Ellipsis className="h-4 w-4" />
@@ -288,10 +203,21 @@ export function TeamsSection({
                       </DropdownMenuItem>
                     </DropdownMenuContent>
                   </DropdownMenu>
-                </div>
-
+                }
+                dataTestId={`team-card-${team.id}`}
+                description={team.description}
+                isSymlink={team.isSymlink}
+                key={team.id}
+                memberCount={team.personaIds.length}
+                personas={resolution.resolvedPersonas}
+                sourceDir={team.sourceDir}
+                symlinkTarget={team.symlinkTarget}
+                teamId={team.id}
+                teamName={team.name}
+                version={team.version}
+              >
                 {hasMissingPersonas ? (
-                  <p className="mt-3 rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
+                  <p className="border-t border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive">
                     {missingPersonaCount} persona
                     {missingPersonaCount === 1 ? "" : "s"} in this team{" "}
                     {missingPersonaCount === 1 ? "is" : "are"} no longer in your
@@ -299,42 +225,68 @@ export function TeamsSection({
                     exporting.
                   </p>
                 ) : null}
-              </Card>
+              </TeamIdentityCard>
             );
           })}
-          <button
-            className="flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-primary p-3 text-primary transition-colors hover:bg-primary/5"
-            onClick={openFilePicker}
-            type="button"
-          >
-            <Upload className="h-4 w-4" />
-            <span className="text-xs">Import</span>
-          </button>
+          <NewTeamCard
+            isPending={isPending}
+            onCreate={onCreate}
+            onImport={openFilePicker}
+            onInstallFromDirectory={onInstallFromDirectory}
+          />
         </div>
       ) : null}
 
-      {!isLoading && teams.length === 0 ? (
-        <button
-          className="w-full cursor-pointer rounded-xl border border-dashed border-primary/40 px-6 py-10 text-center transition-colors hover:border-primary hover:bg-primary/5"
-          onClick={openFilePicker}
-          type="button"
-        >
-          <p className="text-sm font-semibold tracking-tight">No teams yet</p>
-          <p className="mt-2 text-sm text-muted-foreground">
-            Create a team from the personas in My Agents for quick deployment to
-            channels.
-          </p>
-          <p className="mt-1 text-xs text-muted-foreground/70">
-            Or drop a .team.json file here to import.
-          </p>
-        </button>
-      ) : null}
-
       {error ? (
-        <p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
+        <p
+          className={`${TEAM_CARD_COLUMN_CLASS} rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive`}
+        >
           {error.message}
         </p>
       ) : null}
     </section>
   );
 }
+
+function NewTeamCard({
+  isPending,
+  onCreate,
+  onImport,
+  onInstallFromDirectory,
+}: {
+  isPending: boolean;
+  onCreate: () => void;
+  onImport: () => void;
+  onInstallFromDirectory?: () => void;
+}) {
+  return (
+    <DropdownMenu modal={false}>
+      <DropdownMenuTrigger asChild>
+        <CreateIdentityCard
+          ariaLabel="New team"
+          dataTestId="new-team-card"
+          label="New team"
+        />
+      </DropdownMenuTrigger>
+      <DropdownMenuContent
+        align="start"
+        onCloseAutoFocus={(event) => event.preventDefault()}
+      >
+        <DropdownMenuItem disabled={isPending} onClick={onCreate}>
+          Create team
+        </DropdownMenuItem>
+        {onInstallFromDirectory ? (
+          <DropdownMenuItem
+            disabled={isPending}
+            onClick={onInstallFromDirectory}
+          >
+            Install from directory
+          </DropdownMenuItem>
+        ) : null}
+        <DropdownMenuItem disabled={isPending} onClick={onImport}>
+          Import team file
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index eede549567..a63018443b 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -2,22 +2,28 @@ import * as React from "react";
 import {
   ChevronDown,
   ChevronRight,
+  Clipboard,
   Ellipsis,
+  FileText,
   OctagonX,
-  Plus,
+  Pencil,
+  Play,
+  Power,
+  Square,
   Trash2,
+  UserPlus,
 } from "lucide-react";
+import { toast } from "sonner";
 
-import { isPersonaActive } from "@/features/agents/lib/catalog";
 import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
-import { useFeedbackToasts } from "@/shared/hooks/useToastEffect";
-import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
+import { useUserProfileQuery } from "@/features/profile/hooks";
 import type {
   AgentPersona,
   ManagedAgent,
   PresenceLookup,
 } from "@/shared/api/types";
-import { Badge } from "@/shared/ui/badge";
+import { useFeedbackToasts } from "@/shared/hooks/useToastEffect";
+import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
 import { Button } from "@/shared/ui/button";
 import {
   DropdownMenu,
@@ -26,11 +32,10 @@ import {
   DropdownMenuSeparator,
   DropdownMenuTrigger,
 } from "@/shared/ui/dropdown-menu";
-import { Skeleton } from "@/shared/ui/skeleton";
-import { AgentGroupRows } from "./AgentGroupRows";
-import { PersonaActionsMenu } from "./PersonaActionsMenu";
-import { PersonaIdentity } from "./PersonaIdentity";
-import { PersonaLibraryEntryPoints } from "./PersonaLibraryEntryPoints";
+import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
+import { AgentIdentityCard } from "./AgentIdentityCard";
+import { CreateIdentityCard } from "./CreateIdentityCard";
+import { EditAgentDialog } from "./EditAgentDialog";
 
 type UnifiedAgentsSectionProps = {
   actionErrorMessage: string | null;
@@ -52,6 +57,7 @@ type UnifiedAgentsSectionProps = {
   onBulkStopRunning: () => void;
   onCreateAgent: () => void;
   onDeleteAgent: (pubkey: string) => void;
+  onOpenAgentProfile?: (pubkey: string) => void;
   onSelectLogAgent: (pubkey: string | null) => void;
   onStartAgent: (pubkey: string) => void;
   onStopAgent: (pubkey: string) => void;
@@ -76,6 +82,9 @@ type UnifiedAgentsSectionProps = {
 
 type PersonaGroup = { persona: AgentPersona; agents: ManagedAgent[] };
 
+const AGENT_CARD_COLUMN_CLASS = "w-full";
+const AGENT_CARD_GRID_CLASS = `${AGENT_CARD_COLUMN_CLASS} grid grid-cols-[repeat(auto-fill,minmax(220px,240px))] justify-start gap-3`;
+
 function buildUnifiedGroups(personas: AgentPersona[], agents: ManagedAgent[]) {
   const byPersonaId = new Map<string, ManagedAgent[]>();
   const ungrouped: ManagedAgent[] = [];
@@ -109,27 +118,19 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     actionErrorMessage,
     actionNoticeMessage,
     agents,
-    channelIdToName,
-    channelsByPubkey,
     agentsError,
     isActionPending,
     isAgentsLoading,
-    logContent,
-    logError,
-    logLoading,
-    personaLabelsById,
-    presenceLoaded,
-    presenceLookup,
     onAddToChannel,
     onBulkRemoveStopped,
     onBulkStopRunning,
     onCreateAgent,
     onDeleteAgent,
+    onOpenAgentProfile,
     onSelectLogAgent,
     onStartAgent,
     onStopAgent,
     onToggleStartOnAppLaunch,
-    selectedLogAgentPubkey,
     canChooseCatalog,
     personas,
     personasError,
@@ -147,14 +148,28 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     onImportPersonaFile,
   } = props;
 
-  const runningCount = agents.filter((a) => isManagedAgentActive(a)).length;
+  const runningCount = agents.filter((agent) =>
+    isManagedAgentActive(agent),
+  ).length;
   const stoppedCount = agents.filter(
-    (a) => a.status === "stopped" || a.status === "not_deployed",
+    (agent) => agent.status === "stopped" || agent.status === "not_deployed",
   ).length;
   const { groups, ungrouped, unknown } = React.useMemo(
     () => buildUnifiedGroups(personas, agents),
     [personas, agents],
   );
+  const additionalPersonaAgents = React.useMemo(() => {
+    const additional: ManagedAgent[] = [];
+    for (const group of groups) {
+      const primary = pickProfileAgent(group.agents);
+      for (const agent of group.agents) {
+        if (primary?.pubkey !== agent.pubkey) {
+          additional.push(agent);
+        }
+      }
+    }
+    return additional;
+  }, [groups]);
   const [collapsed, setCollapsed] = React.useState<Set<string>>(new Set());
   const {
     fileInputRef,
@@ -176,25 +191,24 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
   useFeedbackToasts(actionNoticeMessage, actionErrorMessage);
   useFeedbackToasts(personaFeedbackNoticeMessage, personaFeedbackErrorMessage);
   const isLoading = isAgentsLoading || isPersonasLoading;
-
-  const rowProps = {
-    channelIdToName,
-    channelsByPubkey,
+  const agentMenuProps = {
     isActionPending,
-    logContent,
-    logError,
-    logLoading,
-    personaLabelsById,
-    presenceLoaded,
-    presenceLookup,
-    selectedLogAgentPubkey,
     onAddToChannel,
     onDelete: onDeleteAgent,
-    onSelectLogAgent,
+    onOpenLogs: onSelectLogAgent,
     onStart: onStartAgent,
     onStop: onStopAgent,
     onToggleStartOnAppLaunch,
   } as const;
+  const personaMenuProps = {
+    isActionPending,
+    isPersonasPending,
+    onDeactivatePersona,
+    onDeletePersona,
+    onDuplicatePersona,
+    onEditPersona,
+    onExportPersona,
+  } as const;
 
   return (
     <section
@@ -212,115 +226,83 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
 
       <SectionHeader
         agentCount={agents.length}
-        canChooseCatalog={canChooseCatalog}
         fileInputRef={fileInputRef}
         handleFileChange={handleFileChange}
         isActionPending={isActionPending}
-        isPersonasPending={isPersonasPending}
-        openFilePicker={openFilePicker}
         runningCount={runningCount}
         stoppedCount={stoppedCount}
         onBulkRemoveStopped={onBulkRemoveStopped}
         onBulkStopRunning={onBulkStopRunning}
-        onChooseCatalog={onChooseCatalog}
-        onCreateAgent={onCreateAgent}
-        onCreatePersona={onCreatePersona}
       />
 
       {isLoading ? <LoadingSkeleton /> : null}
 
-      {!isLoading && personas.length === 0 && agents.length === 0 ? (
-        <EmptyState
-          canChooseCatalog={canChooseCatalog}
-          isPersonasPending={isPersonasPending}
-          openFilePicker={openFilePicker}
-          onChooseCatalog={onChooseCatalog}
-          onCreatePersona={onCreatePersona}
-        />
-      ) : null}
-
-      {!isLoading && (personas.length > 0 || agents.length > 0) ? (
+      {!isLoading ? (
         <div className="space-y-3" data-testid="unified-agents-groups">
-          {groups.map((g) => {
-            const isCollapsed = collapsed.has(g.persona.id);
-            const hasAgents = g.agents.length > 0;
-            const isDeactivated = !isPersonaActive(g.persona);
-            return (
-              <div
-                key={g.persona.id}
-                className={`overflow-hidden rounded-xl border border-border/70 bg-card/40${isDeactivated ? " opacity-60" : ""}`}
-              >
-                <div className="flex items-center gap-2 px-3 py-2 transition-colors hover:bg-muted/40">
-                  <button
-                    className="flex min-w-0 flex-1 items-center gap-2 py-1 text-left"
-                    onClick={() => toggle(g.persona.id)}
-                    type="button"
-                  >
-                    {isCollapsed ? (
-                      <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
-                    ) : (
-                      <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
-                    )}
-                    <PersonaIdentity
-                      persona={g.persona}
-                      showPromptTooltip={false}
-                    />
-                    <span className="shrink-0 text-xs text-muted-foreground">
-                      {hasAgents
-                        ? `${g.agents.length} instance${g.agents.length === 1 ? "" : "s"}`
-                        : "Not deployed"}
-                    </span>
-                  </button>
-                  <div className="flex shrink-0 items-center gap-1">
-                    {isDeactivated ? (
-                      <Badge variant="outline">Deactivated</Badge>
-                    ) : !hasAgents ? (
-                      <Badge variant="outline">Inactive</Badge>
-                    ) : null}
-                    <PersonaActionsMenu
-                      isActionPending={isActionPending}
-                      isPending={isPersonasPending}
-                      persona={g.persona}
-                      onDuplicate={onDuplicatePersona}
-                      onEdit={onEditPersona}
-                      onExport={onExportPersona}
-                      onDeactivate={onDeactivatePersona}
-                      onDelete={onDeletePersona}
-                    />
-                  </div>
-                </div>
-                {!isCollapsed && hasAgents ? (
-                  <AgentGroupRows agents={g.agents} {...rowProps} />
-                ) : null}
-              </div>
-            );
-          })}
+          <div className={AGENT_CARD_GRID_CLASS}>
+            {groups.map((group) => {
+              const profileAgent = pickProfileAgent(group.agents);
+              return (
+                <AgentPersonaCard
+                  agent={profileAgent}
+                  agentMenuProps={agentMenuProps}
+                  key={group.persona.id}
+                  persona={group.persona}
+                  personaMenuProps={personaMenuProps}
+                  onOpenAgentProfile={onOpenAgentProfile}
+                />
+              );
+            })}
+            <NewAgentCard
+              canChooseCatalog={canChooseCatalog}
+              isPersonasPending={isPersonasPending}
+              openFilePicker={openFilePicker}
+              onChooseCatalog={onChooseCatalog}
+              onCreateAgent={onCreateAgent}
+              onCreatePersona={onCreatePersona}
+            />
+          </div>
 
+          {additionalPersonaAgents.length > 0 ? (
+            <CollapsibleAgentGroup
+              agents={additionalPersonaAgents}
+              agentMenuProps={agentMenuProps}
+              collapsed={collapsed}
+              groupKey="__additional_persona_agents__"
+              label="Additional agent instances"
+              onToggle={toggle}
+              onOpenAgentProfile={onOpenAgentProfile}
+            />
+          ) : null}
           {unknown.length > 0 ? (
             <CollapsibleAgentGroup
               agents={unknown}
+              agentMenuProps={agentMenuProps}
               collapsed={collapsed}
               groupKey="__unknown__"
               label="Unknown Persona"
-              rowProps={rowProps}
               onToggle={toggle}
+              onOpenAgentProfile={onOpenAgentProfile}
             />
           ) : null}
           {ungrouped.length > 0 ? (
             <CollapsibleAgentGroup
               agents={ungrouped}
+              agentMenuProps={agentMenuProps}
               collapsed={collapsed}
               groupKey="__ungrouped__"
               label="Custom Agents"
-              rowProps={rowProps}
               onToggle={toggle}
+              onOpenAgentProfile={onOpenAgentProfile}
             />
           ) : null}
         </div>
       ) : null}
 
       {!isLoading && stoppedCount > 0 ? (
-        <div className="flex items-center justify-between rounded-xl border border-border/60 bg-muted/30 px-4 py-2.5">
+        <div
+          className={`${AGENT_CARD_COLUMN_CLASS} flex items-center justify-between rounded-xl border border-border/60 bg-muted/30 px-4 py-2.5`}
+        >
           <p className="text-sm text-muted-foreground">
             {stoppedCount} stopped {stoppedCount === 1 ? "agent" : "agents"}
           </p>
@@ -338,12 +320,16 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
       ) : null}
 
       {agentsError ? (
-        <p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
+        <p
+          className={`${AGENT_CARD_COLUMN_CLASS} rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive`}
+        >
           {agentsError.message}
         </p>
       ) : null}
       {personasError ? (
-        <p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
+        <p
+          className={`${AGENT_CARD_COLUMN_CLASS} rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive`}
+        >
           {personasError.message}
         </p>
       ) : null}
@@ -351,43 +337,417 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
   );
 }
 
+function pickProfileAgent(agents: ManagedAgent[]) {
+  return [...agents].sort((left, right) => {
+    const activeDiff =
+      Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left));
+    if (activeDiff !== 0) return activeDiff;
+    return left.name.localeCompare(right.name);
+  })[0];
+}
+
+type AgentMenuProps = {
+  isActionPending: boolean;
+  onAddToChannel: (agent: ManagedAgent) => void;
+  onDelete: (pubkey: string) => void;
+  onOpenLogs: (pubkey: string) => void;
+  onStart: (pubkey: string) => void;
+  onStop: (pubkey: string) => void;
+  onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
+};
+
+type PersonaMenuProps = {
+  isActionPending: boolean;
+  isPersonasPending: boolean;
+  onDeactivatePersona: (persona: AgentPersona) => void;
+  onDeletePersona: (persona: AgentPersona) => void;
+  onDuplicatePersona: (persona: AgentPersona) => void;
+  onEditPersona: (persona: AgentPersona) => void;
+  onExportPersona: (persona: AgentPersona) => void;
+};
+
+function AgentPersonaCard({
+  agent,
+  agentMenuProps,
+  persona,
+  personaMenuProps,
+  onOpenAgentProfile,
+}: {
+  agent: ManagedAgent | undefined;
+  agentMenuProps: AgentMenuProps;
+  persona: AgentPersona;
+  personaMenuProps: PersonaMenuProps;
+  onOpenAgentProfile?: (pubkey: string) => void;
+}) {
+  const title = persona.displayName;
+  const modelLabel = formatAgentModelLabel(agent?.model ?? persona.model);
+  const profileQuery = useUserProfileQuery(agent?.pubkey);
+  const avatarUrl = agent
+    ? firstAvatarUrl(profileQuery.data?.avatarUrl, persona.avatarUrl)
+    : persona.avatarUrl;
+
+  return (
+    <AgentIdentityCard
+      actions={
+        <AgentPersonaActionsMenu
+          agent={agent}
+          agentMenuProps={agentMenuProps}
+          persona={persona}
+          personaMenuProps={personaMenuProps}
+        />
+      }
+      ariaLabel={`${title} agent profile`}
+      avatarUrl={avatarUrl}
+      dataTestId={`persona-agent-row-${persona.id}`}
+      label={title}
+      modelLabel={modelLabel}
+      onClick={() => {
+        if (agent && onOpenAgentProfile) {
+          onOpenAgentProfile(agent.pubkey);
+          return;
+        }
+        personaMenuProps.onEditPersona(persona);
+      }}
+    />
+  );
+}
+
+function StandaloneAgentCard({
+  agent,
+  agentMenuProps,
+  onOpenAgentProfile,
+}: {
+  agent: ManagedAgent;
+  agentMenuProps: AgentMenuProps;
+  onOpenAgentProfile?: (pubkey: string) => void;
+}) {
+  const title = agent.name;
+  const profileQuery = useUserProfileQuery(agent.pubkey);
+
+  return (
+    <AgentIdentityCard
+      actions={<AgentActionsMenu agent={agent} {...agentMenuProps} />}
+      ariaLabel={`${title} agent profile`}
+      avatarUrl={profileQuery.data?.avatarUrl}
+      dataTestId={`managed-agent-${agent.pubkey}`}
+      label={title}
+      modelLabel={formatAgentModelLabel(agent.model)}
+      onClick={() => {
+        if (onOpenAgentProfile) {
+          onOpenAgentProfile(agent.pubkey);
+        } else {
+          agentMenuProps.onOpenLogs(agent.pubkey);
+        }
+      }}
+    />
+  );
+}
+
+function AgentPersonaActionsMenu({
+  agent,
+  agentMenuProps,
+  persona,
+  personaMenuProps,
+}: {
+  agent: ManagedAgent | undefined;
+  agentMenuProps: AgentMenuProps;
+  persona: AgentPersona;
+  personaMenuProps: PersonaMenuProps;
+}) {
+  const [editOpen, setEditOpen] = React.useState(false);
+  const disabled =
+    personaMenuProps.isActionPending || personaMenuProps.isPersonasPending;
+
+  return (
+    <>
+      <DropdownMenu modal={false}>
+        <DropdownMenuTrigger asChild>
+          <button
+            aria-label={`Open actions for ${persona.displayName}`}
+            className="flex h-7 w-7 items-center justify-center rounded-md bg-background/70 text-muted-foreground transition-colors hover:bg-background hover:text-foreground data-[state=open]:bg-background data-[state=open]:text-foreground"
+            type="button"
+          >
+            <Ellipsis className="h-4 w-4" />
+          </button>
+        </DropdownMenuTrigger>
+        <DropdownMenuContent
+          align="end"
+          onCloseAutoFocus={(event) => event.preventDefault()}
+        >
+          {agent ? (
+            <>
+              <AgentActionItems
+                agent={agent}
+                {...agentMenuProps}
+                onEdit={() => setEditOpen(true)}
+              />
+              <DropdownMenuSeparator />
+            </>
+          ) : null}
+          {!persona.isBuiltIn ? (
+            <DropdownMenuItem
+              disabled={disabled}
+              onClick={() => personaMenuProps.onEditPersona(persona)}
+            >
+              <Pencil className="h-4 w-4" />
+              Edit persona
+            </DropdownMenuItem>
+          ) : null}
+          <DropdownMenuItem
+            disabled={disabled}
+            onClick={() => personaMenuProps.onDuplicatePersona(persona)}
+          >
+            <Clipboard className="h-4 w-4" />
+            Duplicate persona
+          </DropdownMenuItem>
+          <DropdownMenuItem
+            disabled={disabled}
+            onClick={() => personaMenuProps.onExportPersona(persona)}
+          >
+            <FileText className="h-4 w-4" />
+            Export persona
+          </DropdownMenuItem>
+          <DropdownMenuSeparator />
+          {persona.isBuiltIn ? (
+            <DropdownMenuItem
+              className="text-destructive focus:text-destructive"
+              disabled={disabled}
+              onClick={() => personaMenuProps.onDeactivatePersona(persona)}
+            >
+              <Trash2 className="h-4 w-4" />
+              Remove from My Agents
+            </DropdownMenuItem>
+          ) : persona.sourceTeam ? (
+            <DropdownMenuItem disabled>
+              <Trash2 className="h-4 w-4" />
+              Managed by team
+            </DropdownMenuItem>
+          ) : (
+            <DropdownMenuItem
+              className="text-destructive focus:text-destructive"
+              disabled={disabled}
+              onClick={() => personaMenuProps.onDeletePersona(persona)}
+            >
+              <Trash2 className="h-4 w-4" />
+              Delete persona
+            </DropdownMenuItem>
+          )}
+        </DropdownMenuContent>
+      </DropdownMenu>
+
+      {agent ? (
+        <EditAgentDialog
+          agent={agent}
+          onOpenChange={setEditOpen}
+          open={editOpen}
+        />
+      ) : null}
+    </>
+  );
+}
+
+function AgentActionsMenu({
+  agent,
+  isActionPending,
+  onAddToChannel,
+  onDelete,
+  onOpenLogs,
+  onStart,
+  onStop,
+  onToggleStartOnAppLaunch,
+}: { agent: ManagedAgent } & AgentMenuProps) {
+  const [editOpen, setEditOpen] = React.useState(false);
+
+  return (
+    <>
+      <DropdownMenu modal={false}>
+        <DropdownMenuTrigger asChild>
+          <button
+            aria-label={`Agent actions for ${agent.name}`}
+            className="flex h-7 w-7 items-center justify-center rounded-md bg-background/70 text-muted-foreground transition-colors hover:bg-background hover:text-foreground data-[state=open]:bg-background data-[state=open]:text-foreground"
+            data-testid={`managed-agent-actions-${agent.pubkey}`}
+            type="button"
+          >
+            <Ellipsis className="h-4 w-4" />
+          </button>
+        </DropdownMenuTrigger>
+        <DropdownMenuContent
+          align="end"
+          onCloseAutoFocus={(event) => event.preventDefault()}
+        >
+          <AgentActionItems
+            agent={agent}
+            isActionPending={isActionPending}
+            onAddToChannel={onAddToChannel}
+            onDelete={onDelete}
+            onOpenLogs={onOpenLogs}
+            onStart={onStart}
+            onStop={onStop}
+            onToggleStartOnAppLaunch={onToggleStartOnAppLaunch}
+            onEdit={() => setEditOpen(true)}
+          />
+        </DropdownMenuContent>
+      </DropdownMenu>
+
+      <EditAgentDialog
+        agent={agent}
+        onOpenChange={setEditOpen}
+        open={editOpen}
+      />
+    </>
+  );
+}
+
+function AgentActionItems({
+  agent,
+  isActionPending,
+  onAddToChannel,
+  onDelete,
+  onEdit,
+  onOpenLogs,
+  onStart,
+  onStop,
+  onToggleStartOnAppLaunch,
+}: { agent: ManagedAgent; onEdit?: () => void } & AgentMenuProps) {
+  const isActive = isManagedAgentActive(agent);
+
+  return (
+    <>
+      {agent.backend.type === "provider" ? (
+        <>
+          <DropdownMenuItem
+            disabled={isActionPending}
+            onClick={() => onStart(agent.pubkey)}
+          >
+            <Play className="h-4 w-4" />
+            {isActive ? "Redeploy" : "Deploy"}
+          </DropdownMenuItem>
+          <DropdownMenuItem
+            disabled={isActionPending}
+            onClick={() => onStop(agent.pubkey)}
+          >
+            <Square className="h-4 w-4" />
+            Shutdown
+          </DropdownMenuItem>
+        </>
+      ) : isActive ? (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() => onStop(agent.pubkey)}
+        >
+          <Square className="h-4 w-4" />
+          Stop
+        </DropdownMenuItem>
+      ) : (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() => onStart(agent.pubkey)}
+        >
+          <Play className="h-4 w-4" />
+          Spawn
+        </DropdownMenuItem>
+      )}
+
+      {agent.backend.type !== "provider" && onEdit ? (
+        <DropdownMenuItem onClick={onEdit}>
+          <Pencil className="h-4 w-4" />
+          Edit agent
+        </DropdownMenuItem>
+      ) : null}
+
+      <DropdownMenuItem
+        disabled={isActionPending}
+        onClick={() => onAddToChannel(agent)}
+      >
+        <UserPlus className="h-4 w-4" />
+        Add to channel
+      </DropdownMenuItem>
+
+      <DropdownMenuItem
+        onClick={async () => {
+          await navigator.clipboard.writeText(agent.pubkey);
+          toast.success("Copied pubkey to clipboard");
+        }}
+      >
+        <Clipboard className="h-4 w-4" />
+        Copy pubkey
+      </DropdownMenuItem>
+
+      {agent.backend.type === "local" ? (
+        <DropdownMenuItem onClick={() => onOpenLogs(agent.pubkey)}>
+          <FileText className="h-4 w-4" />
+          View logs
+        </DropdownMenuItem>
+      ) : null}
+
+      {agent.backend.type === "local" ? (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() =>
+            onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
+          }
+        >
+          <Power className="h-4 w-4" />
+          {agent.startOnAppLaunch ? "Disable auto-start" : "Enable auto-start"}
+        </DropdownMenuItem>
+      ) : null}
+
+      <DropdownMenuSeparator />
+
+      <DropdownMenuItem
+        className="text-destructive focus:text-destructive"
+        disabled={isActionPending}
+        onClick={() => onDelete(agent.pubkey)}
+      >
+        <Trash2 className="h-4 w-4" />
+        Delete
+      </DropdownMenuItem>
+    </>
+  );
+}
+
+function formatAgentModelLabel(model: string | null | undefined) {
+  const trimmed = model?.trim();
+  return trimmed && trimmed.length > 0 ? trimmed : "Auto";
+}
+
+function firstAvatarUrl(
+  ...candidates: Array<string | null | undefined>
+): string | null {
+  for (const candidate of candidates) {
+    const trimmed = candidate?.trim();
+    if (trimmed) return trimmed;
+  }
+  return null;
+}
+
 function SectionHeader({
   agentCount,
-  canChooseCatalog,
   fileInputRef,
   handleFileChange,
   isActionPending,
-  isPersonasPending,
-  openFilePicker,
   runningCount,
   stoppedCount,
   onBulkRemoveStopped,
   onBulkStopRunning,
-  onChooseCatalog,
-  onCreateAgent,
-  onCreatePersona,
 }: {
   agentCount: number;
-  canChooseCatalog: boolean;
   fileInputRef: React.RefObject<HTMLInputElement | null>;
   handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
   isActionPending: boolean;
-  isPersonasPending: boolean;
-  openFilePicker: () => void;
   runningCount: number;
   stoppedCount: number;
   onBulkRemoveStopped: () => void;
   onBulkStopRunning: () => void;
-  onChooseCatalog: () => void;
-  onCreateAgent: () => void;
-  onCreatePersona: () => void;
 }) {
   return (
-    <div className="flex items-center justify-between gap-3">
+    <div
+      className={`${AGENT_CARD_COLUMN_CLASS} flex items-center justify-between gap-3`}
+    >
       <div>
         <h3 className="text-sm font-semibold tracking-tight">Your Agents</h3>
-        <p className="text-sm text-muted-foreground">
-          Personas and their deployed agent instances.
+        <p className="text-sm text-secondary-foreground/75">
+          Agents in this workspace.
         </p>
       </div>
       <input
@@ -397,181 +757,113 @@ function SectionHeader({
         ref={fileInputRef}
         type="file"
       />
-      <div className="flex items-center gap-2">
-        {agentCount > 0 ? (
-          <DropdownMenu modal={false}>
-            <DropdownMenuTrigger asChild>
-              <Button
-                aria-label="Bulk actions"
-                className="h-7 w-7"
-                size="icon"
-                variant="ghost"
-              >
-                <Ellipsis className="h-4 w-4" />
-              </Button>
-            </DropdownMenuTrigger>
-            <DropdownMenuContent
-              align="end"
-              onCloseAutoFocus={(e) => e.preventDefault()}
-            >
-              <DropdownMenuItem
-                disabled={isActionPending || runningCount === 0}
-                onClick={onBulkStopRunning}
-              >
-                <OctagonX className="h-4 w-4" />
-                Stop all running ({runningCount})
-              </DropdownMenuItem>
-              <DropdownMenuItem
-                className="text-destructive focus:text-destructive"
-                disabled={isActionPending || stoppedCount === 0}
-                onClick={onBulkRemoveStopped}
-              >
-                <Trash2 className="h-4 w-4" />
-                Remove all stopped ({stoppedCount})
-              </DropdownMenuItem>
-            </DropdownMenuContent>
-          </DropdownMenu>
-        ) : null}
+      {agentCount > 0 ? (
         <DropdownMenu modal={false}>
           <DropdownMenuTrigger asChild>
-            <Button size="sm" type="button" variant="default">
-              <Plus className="h-4 w-4" />
-              New
+            <Button
+              aria-label="Bulk actions"
+              className="h-7 w-7"
+              size="icon"
+              variant="ghost"
+            >
+              <Ellipsis className="h-4 w-4" />
             </Button>
           </DropdownMenuTrigger>
           <DropdownMenuContent
             align="end"
-            onCloseAutoFocus={(e) => e.preventDefault()}
+            onCloseAutoFocus={(event) => event.preventDefault()}
           >
             <DropdownMenuItem
-              disabled={isPersonasPending}
-              onClick={onCreatePersona}
+              disabled={isActionPending || runningCount === 0}
+              onClick={onBulkStopRunning}
             >
-              Persona
-            </DropdownMenuItem>
-            {canChooseCatalog ? (
-              <DropdownMenuItem
-                disabled={isPersonasPending}
-                onClick={onChooseCatalog}
-              >
-                Choose from Catalog...
-              </DropdownMenuItem>
-            ) : null}
-            <DropdownMenuSeparator />
-            <DropdownMenuItem onClick={onCreateAgent}>
-              Custom Agent
+              <OctagonX className="h-4 w-4" />
+              Stop all running ({runningCount})
             </DropdownMenuItem>
-            <DropdownMenuItem onClick={openFilePicker}>
-              Import persona file
+            <DropdownMenuItem
+              className="text-destructive focus:text-destructive"
+              disabled={isActionPending || stoppedCount === 0}
+              onClick={onBulkRemoveStopped}
+            >
+              <Trash2 className="h-4 w-4" />
+              Remove all stopped ({stoppedCount})
             </DropdownMenuItem>
           </DropdownMenuContent>
         </DropdownMenu>
-      </div>
-    </div>
-  );
-}
-
-function LoadingSkeleton() {
-  return (
-    <div className="space-y-3">
-      {["a", "b", "c"].map((k, index) => (
-        <div
-          className="overflow-hidden rounded-xl border border-border/70 bg-card/40"
-          key={k}
-        >
-          <div className="flex items-center gap-2 px-3 py-2">
-            <div className="flex min-w-0 flex-1 items-center gap-2 py-1">
-              <Skeleton className="h-4 w-4 shrink-0 rounded-sm" />
-              <Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
-              <div className="flex min-w-0 items-center gap-2">
-                <Skeleton className={index === 2 ? "h-4 w-36" : "h-4 w-32"} />
-                <Skeleton className="h-5 w-14 rounded-full" />
-              </div>
-              <Skeleton className="ml-1 h-3 w-20 shrink-0" />
-            </div>
-            {index === 1 ? (
-              <Skeleton className="h-5 w-16 rounded-full" />
-            ) : null}
-            <Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
-          </div>
-          <div className="divide-y divide-border/50 border-t border-border/50">
-            <div className="flex items-start gap-3 px-4 py-3">
-              <div className="min-w-0 flex-1">
-                <div className="grid gap-3 lg:grid-cols-[minmax(0,1.8fr)_minmax(120px,0.8fr)_minmax(0,1.1fr)] lg:gap-4">
-                  <div className="min-w-0">
-                    <div className="flex items-start gap-3">
-                      <Skeleton className="mt-0.5 h-4 w-4 shrink-0 rounded-sm" />
-                      <Skeleton className="mt-1 h-2 w-2 shrink-0 rounded-full" />
-                      <div className="min-w-0 flex-1">
-                        <div className="flex flex-wrap items-center gap-2">
-                          <Skeleton className="h-4 w-36" />
-                          <Skeleton className="h-5 w-16 rounded-full" />
-                        </div>
-                        <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
-                          <Skeleton className="h-3 w-20" />
-                          <Skeleton className="h-3 w-24" />
-                        </div>
-                        {index === 0 ? (
-                          <div className="mt-1.5 flex flex-wrap items-center gap-1.5">
-                            <Skeleton className="h-5 w-20 rounded-full" />
-                            <Skeleton className="h-5 w-24 rounded-full" />
-                          </div>
-                        ) : null}
-                      </div>
-                    </div>
-                  </div>
-                  <div className="space-y-1.5">
-                    <Skeleton className="h-5 w-20 rounded-full" />
-                    <Skeleton className="h-3 w-24" />
-                  </div>
-                  <div className="space-y-1.5">
-                    <Skeleton className="h-4 w-28" />
-                    <Skeleton className="h-3 w-20" />
-                  </div>
-                </div>
-              </div>
-              <div className="flex shrink-0 items-start gap-2 lg:pt-0.5">
-                <Skeleton className="h-7 w-24 rounded-md" />
-                <Skeleton className="h-7 w-7 rounded-md" />
-              </div>
-            </div>
-          </div>
-        </div>
-      ))}
+      ) : null}
     </div>
   );
 }
 
-function EmptyState({
+function NewAgentCard({
   canChooseCatalog,
   isPersonasPending,
   openFilePicker,
   onChooseCatalog,
+  onCreateAgent,
   onCreatePersona,
 }: {
   canChooseCatalog: boolean;
   isPersonasPending: boolean;
   openFilePicker: () => void;
   onChooseCatalog: () => void;
+  onCreateAgent: () => void;
   onCreatePersona: () => void;
 }) {
   return (
-    <div className="rounded-xl border border-dashed border-primary/40 px-6 py-10 text-center">
-      <p className="text-sm font-semibold tracking-tight">No agents yet</p>
-      <p className="mt-2 text-sm text-muted-foreground">
-        Create a persona or choose one from the catalog, then deploy it to a
-        channel.
-      </p>
-      <div className="mt-4 flex flex-wrap items-center justify-center gap-2">
-        <PersonaLibraryEntryPoints
-          canChooseCatalog={canChooseCatalog}
-          isPending={isPersonasPending}
-          layout="empty"
-          onCreate={onCreatePersona}
-          onChooseCatalog={onChooseCatalog}
-          onImport={openFilePicker}
+    <DropdownMenu modal={false}>
+      <DropdownMenuTrigger asChild>
+        <CreateIdentityCard
+          ariaLabel="New agent"
+          dataTestId="new-agent-card"
+          label="New agent"
         />
-      </div>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent
+        align="start"
+        onCloseAutoFocus={(event) => event.preventDefault()}
+      >
+        <DropdownMenuItem
+          disabled={isPersonasPending}
+          onClick={onCreatePersona}
+        >
+          Persona
+        </DropdownMenuItem>
+        {canChooseCatalog ? (
+          <DropdownMenuItem
+            disabled={isPersonasPending}
+            onClick={onChooseCatalog}
+          >
+            Choose from Catalog...
+          </DropdownMenuItem>
+        ) : null}
+        <DropdownMenuSeparator />
+        <DropdownMenuItem onClick={onCreateAgent}>
+          Custom Agent
+        </DropdownMenuItem>
+        <DropdownMenuItem onClick={openFilePicker}>
+          Import persona file
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
+
+function LoadingSkeleton() {
+  return (
+    <div className={AGENT_CARD_GRID_CLASS}>
+      <IdentityCardSkeleton
+        footerSubtitleWidthClass="w-14"
+        footerTitleWidthClass="w-24"
+      />
+      <IdentityCardSkeleton
+        footerSubtitleWidthClass="w-20"
+        footerTitleWidthClass="w-32"
+      />
+      <IdentityCardSkeleton
+        footerSubtitleWidthClass="w-16"
+        footerTitleWidthClass="w-28"
+      />
     </div>
   );
 }
@@ -580,38 +872,47 @@ function CollapsibleAgentGroup({
   groupKey,
   label,
   agents,
+  agentMenuProps,
   collapsed,
   onToggle,
-  rowProps,
+  onOpenAgentProfile,
 }: {
   groupKey: string;
   label: string;
   agents: ManagedAgent[];
+  agentMenuProps: AgentMenuProps;
   collapsed: ReadonlySet<string>;
   onToggle: (key: string) => void;
-  rowProps: Omit<React.ComponentProps<typeof AgentGroupRows>, "agents">;
+  onOpenAgentProfile?: (pubkey: string) => void;
 }) {
   const isCollapsed = collapsed.has(groupKey);
   return (
-    <div className="overflow-hidden rounded-xl border border-border/70 bg-card/40">
-      <div className="px-3 py-2 transition-colors hover:bg-muted/40">
-        <button
-          className="flex w-full items-center gap-2 py-1 text-left"
-          onClick={() => onToggle(groupKey)}
-          type="button"
-        >
-          {isCollapsed ? (
-            <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
-          ) : (
-            <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
-          )}
-          <span className="text-sm font-medium">{label}</span>
-          <span className="text-xs text-muted-foreground">
-            ({agents.length})
-          </span>
-        </button>
-      </div>
-      {!isCollapsed ? <AgentGroupRows agents={agents} {...rowProps} /> : null}
+    <div className={`${AGENT_CARD_COLUMN_CLASS} space-y-2`}>
+      <button
+        className="group flex items-center gap-2 rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/50"
+        onClick={() => onToggle(groupKey)}
+        type="button"
+      >
+        {isCollapsed ? (
+          <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
+        ) : (
+          <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
+        )}
+        <span className="text-sm font-medium">{label}</span>
+        <span className="text-xs text-muted-foreground">({agents.length})</span>
+      </button>
+      {!isCollapsed ? (
+        <div className={AGENT_CARD_GRID_CLASS}>
+          {agents.map((agent) => (
+            <StandaloneAgentCard
+              agent={agent}
+              agentMenuProps={agentMenuProps}
+              key={agent.pubkey}
+              onOpenAgentProfile={onOpenAgentProfile}
+            />
+          ))}
+        </div>
+      ) : null}
     </div>
   );
 }
diff --git a/desktop/src/shared/ui/identity-card-skeleton.tsx b/desktop/src/shared/ui/identity-card-skeleton.tsx
new file mode 100644
index 0000000000..86f33421e3
--- /dev/null
+++ b/desktop/src/shared/ui/identity-card-skeleton.tsx
@@ -0,0 +1,44 @@
+import { cn } from "@/shared/lib/cn";
+import { Skeleton } from "@/shared/ui/skeleton";
+
+type IdentityCardSkeletonProps = {
+  className?: string;
+  footerSubtitleWidthClass?: string;
+  footerTitleWidthClass?: string;
+  showAction?: boolean;
+};
+
+export function IdentityCardSkeleton({
+  className,
+  footerSubtitleWidthClass = "w-16",
+  footerTitleWidthClass = "w-28",
+  showAction = false,
+}: IdentityCardSkeletonProps) {
+  return (
+    <div
+      className={cn(
+        "relative aspect-[4/5] w-full min-w-0 overflow-hidden rounded-xl border border-border/70 bg-muted/50 shadow-xs",
+        className,
+      )}
+    >
+      {showAction ? (
+        <Skeleton className="absolute top-3 right-3 z-30 h-7 w-7 rounded-md bg-background/70" />
+      ) : null}
+
+      <SingleAvatarSkeleton />
+
+      <div className="absolute right-3 bottom-3 left-3 z-30 flex min-w-0 flex-col gap-1 text-left">
+        <Skeleton className={cn("h-4 max-w-full", footerTitleWidthClass)} />
+        <Skeleton className={cn("h-4 max-w-full", footerSubtitleWidthClass)} />
+      </div>
+    </div>
+  );
+}
+
+function SingleAvatarSkeleton() {
+  return (
+    <div className="absolute inset-x-0 top-0 bottom-12 flex items-center justify-center">
+      <Skeleton className="h-24 w-24 rounded-full" />
+    </div>
+  );
+}

From ae2f3a2e3b702fc3c669b8f723ca512a2dc8f1c6 Mon Sep 17 00:00:00 2001
From: klopez4212 <klopez4212@gmail.com>
Date: Thu, 25 Jun 2026 13:39:16 +0100
Subject: [PATCH 2/6] Restore agent card status and logs

---
 .../features/agents/ui/AgentIdentityCard.tsx  |  8 ++
 .../features/agents/ui/TeamIdentityCard.tsx   | 20 +++-
 .../agents/ui/UnifiedAgentsSection.tsx        | 94 +++++++++++++++++++
 desktop/tests/e2e/agents.spec.ts              |  5 +-
 desktop/tests/e2e/mesh-compute.spec.ts        |  5 +-
 desktop/tests/e2e/persona-env-vars.spec.ts    |  5 +-
 desktop/tests/e2e/smoke.spec.ts               |  5 +-
 7 files changed, 125 insertions(+), 17 deletions(-)

diff --git a/desktop/src/features/agents/ui/AgentIdentityCard.tsx b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
index a66fb9cd7e..8dea118cb1 100644
--- a/desktop/src/features/agents/ui/AgentIdentityCard.tsx
+++ b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
@@ -12,6 +12,7 @@ type AgentIdentityCardProps = {
   label: string;
   modelLabel: string;
   onClick: () => void;
+  status?: ReactNode;
 };
 
 export function AgentIdentityCard({
@@ -22,6 +23,7 @@ export function AgentIdentityCard({
   label,
   modelLabel,
   onClick,
+  status,
 }: AgentIdentityCardProps) {
   const trimmedAvatarUrl = avatarUrl?.trim() || null;
 
@@ -56,6 +58,12 @@ export function AgentIdentityCard({
         <div className="absolute top-3 right-3 z-40">{actions}</div>
       ) : null}
 
+      {status ? (
+        <div className="absolute top-3 left-3 z-30 flex max-w-[calc(100%-4rem)] flex-wrap items-center gap-1.5">
+          {status}
+        </div>
+      ) : null}
+
       <div className="absolute right-3 bottom-3 left-3 z-30 flex min-w-0 flex-col gap-0.5 text-left text-sm leading-5">
         <span className="min-w-0 truncate font-semibold text-foreground tracking-normal">
           {label}
diff --git a/desktop/src/features/agents/ui/TeamIdentityCard.tsx b/desktop/src/features/agents/ui/TeamIdentityCard.tsx
index 17d129e6f8..9255f92822 100644
--- a/desktop/src/features/agents/ui/TeamIdentityCard.tsx
+++ b/desktop/src/features/agents/ui/TeamIdentityCard.tsx
@@ -1,5 +1,5 @@
 import type { ReactNode } from "react";
-import { Link, Users } from "lucide-react";
+import { Info, Link, Users } from "lucide-react";
 
 import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
 import type { AgentPersona } from "@/shared/api/types";
@@ -28,6 +28,7 @@ export function TeamIdentityCard({
   actions,
   children,
   dataTestId,
+  description,
   isSymlink = false,
   memberCount,
   personas,
@@ -37,6 +38,7 @@ export function TeamIdentityCard({
   version,
 }: TeamIdentityCardProps) {
   const footerModelLabel = getTeamFooterModelLabel(personas);
+  const trimmedDescription = description?.trim();
 
   return (
     <Card
@@ -62,6 +64,22 @@ export function TeamIdentityCard({
               v{version}
             </span>
           ) : null}
+          {trimmedDescription ? (
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <button
+                  aria-label={`${teamName} description`}
+                  className="flex h-6 w-6 items-center justify-center rounded-full border border-border/65 bg-background/90 text-muted-foreground shadow-xs"
+                  type="button"
+                >
+                  <Info className="h-3.5 w-3.5" />
+                </button>
+              </TooltipTrigger>
+              <TooltipContent side="bottom" className="max-w-xs">
+                <p>{trimmedDescription}</p>
+              </TooltipContent>
+            </Tooltip>
+          ) : null}
         </div>
 
         <div className="absolute top-3 right-3 z-40">{actions}</div>
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index a63018443b..93ab5509d7 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -15,7 +15,9 @@ import {
 } from "lucide-react";
 import { toast } from "sonner";
 
+import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore";
 import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
+import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
 import { useUserProfileQuery } from "@/features/profile/hooks";
 import type {
   AgentPersona,
@@ -24,6 +26,7 @@ import type {
 } from "@/shared/api/types";
 import { useFeedbackToasts } from "@/shared/hooks/useToastEffect";
 import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
+import { normalizePubkey } from "@/shared/lib/pubkey";
 import { Button } from "@/shared/ui/button";
 import {
   DropdownMenu,
@@ -36,6 +39,7 @@ import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
 import { AgentIdentityCard } from "./AgentIdentityCard";
 import { CreateIdentityCard } from "./CreateIdentityCard";
 import { EditAgentDialog } from "./EditAgentDialog";
+import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel";
 
 type UnifiedAgentsSectionProps = {
   actionErrorMessage: string | null;
@@ -121,6 +125,12 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     agentsError,
     isActionPending,
     isAgentsLoading,
+    logContent,
+    logError,
+    logLoading,
+    presenceLoaded,
+    presenceLookup,
+    selectedLogAgentPubkey,
     onAddToChannel,
     onBulkRemoveStopped,
     onBulkStopRunning,
@@ -170,6 +180,14 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     }
     return additional;
   }, [groups]);
+  const selectedLogAgent = React.useMemo(
+    () =>
+      selectedLogAgentPubkey
+        ? (agents.find((agent) => agent.pubkey === selectedLogAgentPubkey) ??
+          null)
+        : null,
+    [agents, selectedLogAgentPubkey],
+  );
   const [collapsed, setCollapsed] = React.useState<Set<string>>(new Set());
   const {
     fileInputRef,
@@ -249,6 +267,8 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
                   key={group.persona.id}
                   persona={group.persona}
                   personaMenuProps={personaMenuProps}
+                  presenceLoaded={presenceLoaded}
+                  presenceLookup={presenceLookup}
                   onOpenAgentProfile={onOpenAgentProfile}
                 />
               );
@@ -270,6 +290,8 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
               collapsed={collapsed}
               groupKey="__additional_persona_agents__"
               label="Additional agent instances"
+              presenceLoaded={presenceLoaded}
+              presenceLookup={presenceLookup}
               onToggle={toggle}
               onOpenAgentProfile={onOpenAgentProfile}
             />
@@ -281,6 +303,8 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
               collapsed={collapsed}
               groupKey="__unknown__"
               label="Unknown Persona"
+              presenceLoaded={presenceLoaded}
+              presenceLookup={presenceLookup}
               onToggle={toggle}
               onOpenAgentProfile={onOpenAgentProfile}
             />
@@ -292,10 +316,25 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
               collapsed={collapsed}
               groupKey="__ungrouped__"
               label="Custom Agents"
+              presenceLoaded={presenceLoaded}
+              presenceLookup={presenceLookup}
               onToggle={toggle}
               onOpenAgentProfile={onOpenAgentProfile}
             />
           ) : null}
+          {selectedLogAgent ? (
+            <div
+              className={AGENT_CARD_COLUMN_CLASS}
+              data-testid="managed-agent-log-row"
+            >
+              <ManagedAgentLogPanel
+                error={logError}
+                isLoading={logLoading}
+                logContent={logContent}
+                selectedAgent={selectedLogAgent}
+              />
+            </div>
+          ) : null}
         </div>
       ) : null}
 
@@ -371,12 +410,16 @@ function AgentPersonaCard({
   agentMenuProps,
   persona,
   personaMenuProps,
+  presenceLoaded,
+  presenceLookup,
   onOpenAgentProfile,
 }: {
   agent: ManagedAgent | undefined;
   agentMenuProps: AgentMenuProps;
   persona: AgentPersona;
   personaMenuProps: PersonaMenuProps;
+  presenceLoaded: boolean;
+  presenceLookup: PresenceLookup;
   onOpenAgentProfile?: (pubkey: string) => void;
 }) {
   const title = persona.displayName;
@@ -408,6 +451,15 @@ function AgentPersonaCard({
         }
         personaMenuProps.onEditPersona(persona);
       }}
+      status={
+        agent ? (
+          <AgentCardStatus
+            agent={agent}
+            presenceLoaded={presenceLoaded}
+            presenceLookup={presenceLookup}
+          />
+        ) : null
+      }
     />
   );
 }
@@ -415,10 +467,14 @@ function AgentPersonaCard({
 function StandaloneAgentCard({
   agent,
   agentMenuProps,
+  presenceLoaded,
+  presenceLookup,
   onOpenAgentProfile,
 }: {
   agent: ManagedAgent;
   agentMenuProps: AgentMenuProps;
+  presenceLoaded: boolean;
+  presenceLookup: PresenceLookup;
   onOpenAgentProfile?: (pubkey: string) => void;
 }) {
   const title = agent.name;
@@ -439,6 +495,35 @@ function StandaloneAgentCard({
           agentMenuProps.onOpenLogs(agent.pubkey);
         }
       }}
+      status={
+        <AgentCardStatus
+          agent={agent}
+          presenceLoaded={presenceLoaded}
+          presenceLookup={presenceLookup}
+        />
+      }
+    />
+  );
+}
+
+function AgentCardStatus({
+  agent,
+  presenceLoaded,
+  presenceLookup,
+}: {
+  agent: ManagedAgent;
+  presenceLoaded: boolean;
+  presenceLookup: PresenceLookup;
+}) {
+  const activeTurns = useActiveAgentTurns(agent.pubkey);
+  const presenceStatus = presenceLookup[normalizePubkey(agent.pubkey)];
+
+  return (
+    <AgentStatusBadge
+      isWorking={activeTurns.length > 0}
+      presenceLoaded={presenceLoaded}
+      presenceStatus={presenceStatus}
+      status={agent.status}
     />
   );
 }
@@ -465,6 +550,9 @@ function AgentPersonaActionsMenu({
           <button
             aria-label={`Open actions for ${persona.displayName}`}
             className="flex h-7 w-7 items-center justify-center rounded-md bg-background/70 text-muted-foreground transition-colors hover:bg-background hover:text-foreground data-[state=open]:bg-background data-[state=open]:text-foreground"
+            data-testid={
+              agent ? `managed-agent-actions-${agent.pubkey}` : undefined
+            }
             type="button"
           >
             <Ellipsis className="h-4 w-4" />
@@ -874,6 +962,8 @@ function CollapsibleAgentGroup({
   agents,
   agentMenuProps,
   collapsed,
+  presenceLoaded,
+  presenceLookup,
   onToggle,
   onOpenAgentProfile,
 }: {
@@ -882,6 +972,8 @@ function CollapsibleAgentGroup({
   agents: ManagedAgent[];
   agentMenuProps: AgentMenuProps;
   collapsed: ReadonlySet<string>;
+  presenceLoaded: boolean;
+  presenceLookup: PresenceLookup;
   onToggle: (key: string) => void;
   onOpenAgentProfile?: (pubkey: string) => void;
 }) {
@@ -908,6 +1000,8 @@ function CollapsibleAgentGroup({
               agent={agent}
               agentMenuProps={agentMenuProps}
               key={agent.pubkey}
+              presenceLoaded={presenceLoaded}
+              presenceLookup={presenceLookup}
               onOpenAgentProfile={onOpenAgentProfile}
             />
           ))}
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts
index ab4d1dfa30..f55c573b71 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -30,10 +30,7 @@ async function gotoApp(page: import("@playwright/test").Page) {
 }
 
 async function openPersonaCatalog(page: import("@playwright/test").Page) {
-  await page
-    .getByTestId("agents-library-personas")
-    .getByRole("button", { name: "New", exact: true })
-    .click();
+  await page.getByTestId("new-agent-card").click();
   await page.getByText("Choose from Catalog...").click();
 }
 
diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts
index 05ba8e0f2f..e1e7715da3 100644
--- a/desktop/tests/e2e/mesh-compute.spec.ts
+++ b/desktop/tests/e2e/mesh-compute.spec.ts
@@ -76,10 +76,7 @@ async function openManagedAgentActions(
 }
 
 async function openNewAgentMenu(page: import("@playwright/test").Page) {
-  await page
-    .getByTestId("agents-library-personas")
-    .getByRole("button", { name: "New", exact: true })
-    .click();
+  await page.getByTestId("new-agent-card").click();
 }
 
 test.beforeEach(async ({ page }) => {
diff --git a/desktop/tests/e2e/persona-env-vars.spec.ts b/desktop/tests/e2e/persona-env-vars.spec.ts
index e2b25f1bd3..1d837599a7 100644
--- a/desktop/tests/e2e/persona-env-vars.spec.ts
+++ b/desktop/tests/e2e/persona-env-vars.spec.ts
@@ -223,10 +223,7 @@ test("env vars editor renders in PersonaDialog new-persona form", async ({
 
   // Open the Agents view, click New > Persona to open the persona dialog.
   await page.getByTestId("open-agents-view").click();
-  await page
-    .getByTestId("agents-library-personas")
-    .getByRole("button", { name: "New", exact: true })
-    .click();
+  await page.getByTestId("new-agent-card").click();
   await page.getByRole("menuitem", { name: /^Persona$/ }).click();
 
   // The env vars editor should be present.
diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts
index 7e47b76ae8..64e48550db 100644
--- a/desktop/tests/e2e/smoke.spec.ts
+++ b/desktop/tests/e2e/smoke.spec.ts
@@ -115,10 +115,7 @@ test("create agent supports parallelism and system prompt overrides", async ({
 
   await page.goto("/");
   await page.getByTestId("open-agents-view").click();
-  await page
-    .getByTestId("agents-library-personas")
-    .getByRole("button", { name: "New", exact: true })
-    .click();
+  await page.getByTestId("new-agent-card").click();
   await page.getByText("Custom Agent").click();
 
   await page.getByTestId("agent-name-input").fill(agentName);

From a4588ca00d703db260b7a93c7b481dfc6418bed4 Mon Sep 17 00:00:00 2001
From: klopez4212 <klopez4212@gmail.com>
Date: Thu, 25 Jun 2026 14:12:03 +0100
Subject: [PATCH 3/6] Split unified agent grouping helpers

---
 .../agents/ui/UnifiedAgentsSection.tsx        | 40 +----------------
 .../features/agents/ui/unifiedAgentGroups.ts  | 44 +++++++++++++++++++
 2 files changed, 45 insertions(+), 39 deletions(-)
 create mode 100644 desktop/src/features/agents/ui/unifiedAgentGroups.ts

diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index 93ab5509d7..22d6b0d180 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -40,6 +40,7 @@ import { AgentIdentityCard } from "./AgentIdentityCard";
 import { CreateIdentityCard } from "./CreateIdentityCard";
 import { EditAgentDialog } from "./EditAgentDialog";
 import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel";
+import { buildUnifiedGroups, pickProfileAgent } from "./unifiedAgentGroups";
 
 type UnifiedAgentsSectionProps = {
   actionErrorMessage: string | null;
@@ -84,39 +85,9 @@ type UnifiedAgentsSectionProps = {
   onImportPersonaFile: (fileBytes: number[], fileName: string) => void;
 };
 
-type PersonaGroup = { persona: AgentPersona; agents: ManagedAgent[] };
-
 const AGENT_CARD_COLUMN_CLASS = "w-full";
 const AGENT_CARD_GRID_CLASS = `${AGENT_CARD_COLUMN_CLASS} grid grid-cols-[repeat(auto-fill,minmax(220px,240px))] justify-start gap-3`;
 
-function buildUnifiedGroups(personas: AgentPersona[], agents: ManagedAgent[]) {
-  const byPersonaId = new Map<string, ManagedAgent[]>();
-  const ungrouped: ManagedAgent[] = [];
-
-  for (const agent of agents) {
-    if (!agent.personaId) {
-      ungrouped.push(agent);
-    } else {
-      const list = byPersonaId.get(agent.personaId) ?? [];
-      list.push(agent);
-      byPersonaId.set(agent.personaId, list);
-    }
-  }
-
-  const matched = new Set<string>();
-  const groups: PersonaGroup[] = personas.map((p) => {
-    matched.add(p.id);
-    return { persona: p, agents: byPersonaId.get(p.id) ?? [] };
-  });
-
-  const unknown: ManagedAgent[] = [];
-  for (const [id, list] of byPersonaId) {
-    if (!matched.has(id)) unknown.push(...list);
-  }
-
-  return { groups, ungrouped, unknown };
-}
-
 export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
   const {
     actionErrorMessage,
@@ -376,15 +347,6 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
   );
 }
 
-function pickProfileAgent(agents: ManagedAgent[]) {
-  return [...agents].sort((left, right) => {
-    const activeDiff =
-      Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left));
-    if (activeDiff !== 0) return activeDiff;
-    return left.name.localeCompare(right.name);
-  })[0];
-}
-
 type AgentMenuProps = {
   isActionPending: boolean;
   onAddToChannel: (agent: ManagedAgent) => void;
diff --git a/desktop/src/features/agents/ui/unifiedAgentGroups.ts b/desktop/src/features/agents/ui/unifiedAgentGroups.ts
new file mode 100644
index 0000000000..a2d1b987f3
--- /dev/null
+++ b/desktop/src/features/agents/ui/unifiedAgentGroups.ts
@@ -0,0 +1,44 @@
+import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
+import type { AgentPersona, ManagedAgent } from "@/shared/api/types";
+
+type PersonaGroup = { persona: AgentPersona; agents: ManagedAgent[] };
+
+export function buildUnifiedGroups(
+  personas: AgentPersona[],
+  agents: ManagedAgent[],
+) {
+  const byPersonaId = new Map<string, ManagedAgent[]>();
+  const ungrouped: ManagedAgent[] = [];
+
+  for (const agent of agents) {
+    if (!agent.personaId) {
+      ungrouped.push(agent);
+    } else {
+      const list = byPersonaId.get(agent.personaId) ?? [];
+      list.push(agent);
+      byPersonaId.set(agent.personaId, list);
+    }
+  }
+
+  const matched = new Set<string>();
+  const groups: PersonaGroup[] = personas.map((persona) => {
+    matched.add(persona.id);
+    return { persona, agents: byPersonaId.get(persona.id) ?? [] };
+  });
+
+  const unknown: ManagedAgent[] = [];
+  for (const [id, list] of byPersonaId) {
+    if (!matched.has(id)) unknown.push(...list);
+  }
+
+  return { groups, ungrouped, unknown };
+}
+
+export function pickProfileAgent(agents: ManagedAgent[]) {
+  return [...agents].sort((left, right) => {
+    const activeDiff =
+      Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left));
+    if (activeDiff !== 0) return activeDiff;
+    return left.name.localeCompare(right.name);
+  })[0];
+}

From b6ebac07bbe973afa5fd8f342d1c3fe2cfc13e5d Mon Sep 17 00:00:00 2001
From: klopez4212 <klopez4212@gmail.com>
Date: Thu, 25 Jun 2026 14:21:04 +0100
Subject: [PATCH 4/6] Restore card model and error affordances

---
 .../features/agents/ui/AgentIdentityCard.tsx  | 20 ++++++++++++++++---
 .../agents/ui/IdentityInitialsAvatar.tsx      |  4 ++--
 .../agents/ui/UnifiedAgentsSection.tsx        | 10 ++++++++++
 3 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/desktop/src/features/agents/ui/AgentIdentityCard.tsx b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
index 8dea118cb1..5c27d54518 100644
--- a/desktop/src/features/agents/ui/AgentIdentityCard.tsx
+++ b/desktop/src/features/agents/ui/AgentIdentityCard.tsx
@@ -10,6 +10,8 @@ type AgentIdentityCardProps = {
   avatarUrl?: string | null;
   dataTestId: string;
   label: string;
+  errorLabel?: string | null;
+  modelControl?: ReactNode;
   modelLabel: string;
   onClick: () => void;
   status?: ReactNode;
@@ -20,7 +22,9 @@ export function AgentIdentityCard({
   ariaLabel,
   avatarUrl,
   dataTestId,
+  errorLabel,
   label,
+  modelControl,
   modelLabel,
   onClick,
   status,
@@ -68,9 +72,19 @@ export function AgentIdentityCard({
         <span className="min-w-0 truncate font-semibold text-foreground tracking-normal">
           {label}
         </span>
-        <span className="min-w-0 truncate font-normal text-secondary-foreground/75">
-          {modelLabel}
-        </span>
+        {modelControl ?? (
+          <span className="min-w-0 truncate font-normal text-secondary-foreground/75">
+            {modelLabel}
+          </span>
+        )}
+        {errorLabel ? (
+          <span
+            className="min-w-0 truncate text-2xs font-medium text-destructive"
+            title={errorLabel}
+          >
+            {errorLabel}
+          </span>
+        ) : null}
       </div>
     </div>
   );
diff --git a/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx b/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
index 71fa310656..0381ebc03b 100644
--- a/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
+++ b/desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx
@@ -34,16 +34,16 @@ export function IdentityInitialsAvatar({
     IDENTITY_INITIAL_AVATAR_CLASS_NAMES[
       paletteIndex % IDENTITY_INITIAL_AVATAR_CLASS_NAMES.length
     ];
-  const fontSize = Math.round(Math.min(40, Math.max(22, size * 0.28)));
+  const textSizeClassName = size >= 80 ? "text-3xl" : "text-xl";
 
   return (
     <span
       className={cn(
         "flex h-full w-full items-center justify-center rounded-full border-[3px] border-background font-semibold shadow-sm",
         colorClassName,
+        textSizeClassName,
         className,
       )}
-      style={{ fontSize }}
     >
       {initials.length > 0 ? initials : <UserRound className="h-8 w-8" />}
     </span>
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index 22d6b0d180..7c95dadebd 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -16,8 +16,10 @@ import {
 import { toast } from "sonner";
 
 import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore";
+import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError";
 import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
 import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
+import { ModelPicker } from "@/features/agents/ui/ModelPicker";
 import { useUserProfileQuery } from "@/features/profile/hooks";
 import type {
   AgentPersona,
@@ -390,6 +392,9 @@ function AgentPersonaCard({
   const avatarUrl = agent
     ? firstAvatarUrl(profileQuery.data?.avatarUrl, persona.avatarUrl)
     : persona.avatarUrl;
+  const friendlyError = agent
+    ? friendlyAgentLastError(agent.lastError)?.copy
+    : null;
 
   return (
     <AgentIdentityCard
@@ -404,7 +409,9 @@ function AgentPersonaCard({
       ariaLabel={`${title} agent profile`}
       avatarUrl={avatarUrl}
       dataTestId={`persona-agent-row-${persona.id}`}
+      errorLabel={friendlyError}
       label={title}
+      modelControl={agent ? <ModelPicker agent={agent} /> : undefined}
       modelLabel={modelLabel}
       onClick={() => {
         if (agent && onOpenAgentProfile) {
@@ -441,6 +448,7 @@ function StandaloneAgentCard({
 }) {
   const title = agent.name;
   const profileQuery = useUserProfileQuery(agent.pubkey);
+  const friendlyError = friendlyAgentLastError(agent.lastError)?.copy;
 
   return (
     <AgentIdentityCard
@@ -448,7 +456,9 @@ function StandaloneAgentCard({
       ariaLabel={`${title} agent profile`}
       avatarUrl={profileQuery.data?.avatarUrl}
       dataTestId={`managed-agent-${agent.pubkey}`}
+      errorLabel={friendlyError}
       label={title}
+      modelControl={<ModelPicker agent={agent} />}
       modelLabel={formatAgentModelLabel(agent.model)}
       onClick={() => {
         if (onOpenAgentProfile) {

From d2da355455298a6e23be6591bc004f0fded8e969 Mon Sep 17 00:00:00 2001
From: klopez4212 <klopez4212@gmail.com>
Date: Thu, 25 Jun 2026 19:20:38 +0100
Subject: [PATCH 5/6] Guard agent card click fallbacks

---
 desktop/src/features/agents/ui/UnifiedAgentsSection.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index 7c95dadebd..cdce927572 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -418,7 +418,9 @@ function AgentPersonaCard({
           onOpenAgentProfile(agent.pubkey);
           return;
         }
-        personaMenuProps.onEditPersona(persona);
+        if (!persona.isBuiltIn) {
+          personaMenuProps.onEditPersona(persona);
+        }
       }}
       status={
         agent ? (
@@ -463,7 +465,7 @@ function StandaloneAgentCard({
       onClick={() => {
         if (onOpenAgentProfile) {
           onOpenAgentProfile(agent.pubkey);
-        } else {
+        } else if (agent.backend.type === "local") {
           agentMenuProps.onOpenLogs(agent.pubkey);
         }
       }}

From d7fd6f16279a7f86832a59289940aad1abe4ec77 Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
 <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Date: Fri, 26 Jun 2026 00:47:33 -0700
Subject: [PATCH 6/6] feat(agents): add "save as persona template" to agent
 card + sidebar
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Opt-in promote of an existing managed agent into a reusable persona
template. Lands on two surfaces — the agent card overflow menu (standalone
agents only) and the sidebar agent profile's overflow menu — sharing one
`saveAsPersonaTemplateDialogState` + the existing PersonaDialog and
create-persona mutation. No new backend or IPC.

The promote is near-lossless: name, system prompt, model, provider, and env
vars copy across; the resolved harness command reverse-maps to a runtime id;
namePool starts empty for the user to fill in the dialog.

UI says "persona template" while the backend stays `persona` (kind:30175,
builtin:*, .persona.md) — a boundary comment in personaLibraryCopy.ts records
the mapping so the names are not conflated.

Pure refactors to stay under the 1000-line file limit: extract AgentActionItems
(+ AgentMenuProps) and the profile quick-action buttons into sibling files.

Adds regression tests for the dialog-state mapping and a guard that default
agent create stays persona-less.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
---
 .../features/agents/ui/AgentActionItems.tsx   | 155 ++++++++++++++++++
 desktop/src/features/agents/ui/AgentsView.tsx |   1 +
 .../agents/ui/UnifiedAgentsSection.tsx        | 129 +--------------
 .../agents/ui/personaDialogState.test.mjs     | 128 +++++++++++++++
 .../features/agents/ui/personaDialogState.ts  |  55 +++++++
 .../features/agents/ui/personaLibraryCopy.ts  |   4 +
 .../features/agents/ui/usePersonaActions.ts   |  14 ++
 .../agents/ui/useSaveAsPersonaTemplate.ts     |  90 ++++++++++
 .../profile/ui/ProfileQuickActions.tsx        |  88 ++++++++++
 .../features/profile/ui/UserProfilePanel.tsx  |  21 +++
 .../profile/ui/UserProfilePanelSections.tsx   |  93 ++++++-----
 11 files changed, 608 insertions(+), 170 deletions(-)
 create mode 100644 desktop/src/features/agents/ui/AgentActionItems.tsx
 create mode 100644 desktop/src/features/agents/ui/useSaveAsPersonaTemplate.ts
 create mode 100644 desktop/src/features/profile/ui/ProfileQuickActions.tsx

diff --git a/desktop/src/features/agents/ui/AgentActionItems.tsx b/desktop/src/features/agents/ui/AgentActionItems.tsx
new file mode 100644
index 0000000000..5a2718e308
--- /dev/null
+++ b/desktop/src/features/agents/ui/AgentActionItems.tsx
@@ -0,0 +1,155 @@
+import {
+  BookmarkPlus,
+  Clipboard,
+  FileText,
+  Pencil,
+  Play,
+  Power,
+  Square,
+  Trash2,
+  UserPlus,
+} from "lucide-react";
+import { toast } from "sonner";
+
+import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
+import type { ManagedAgent } from "@/shared/api/types";
+import {
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+} from "@/shared/ui/dropdown-menu";
+
+export type AgentMenuProps = {
+  isActionPending: boolean;
+  onAddToChannel: (agent: ManagedAgent) => void;
+  onDelete: (pubkey: string) => void;
+  onOpenLogs: (pubkey: string) => void;
+  onSaveAsTemplate: (agent: ManagedAgent) => void;
+  onStart: (pubkey: string) => void;
+  onStop: (pubkey: string) => void;
+  onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
+};
+
+/**
+ * The shared dropdown-menu item list for a managed agent. Rendered inside the
+ * standalone-agent menu, the persona-backed-agent menu, and the agent card
+ * menu — kept here so all three stay in lockstep.
+ */
+export function AgentActionItems({
+  agent,
+  isActionPending,
+  onAddToChannel,
+  onDelete,
+  onEdit,
+  onOpenLogs,
+  onSaveAsTemplate,
+  onStart,
+  onStop,
+  onToggleStartOnAppLaunch,
+}: { agent: ManagedAgent; onEdit?: () => void } & AgentMenuProps) {
+  const isActive = isManagedAgentActive(agent);
+
+  return (
+    <>
+      {agent.backend.type === "provider" ? (
+        <>
+          <DropdownMenuItem
+            disabled={isActionPending}
+            onClick={() => onStart(agent.pubkey)}
+          >
+            <Play className="h-4 w-4" />
+            {isActive ? "Redeploy" : "Deploy"}
+          </DropdownMenuItem>
+          <DropdownMenuItem
+            disabled={isActionPending}
+            onClick={() => onStop(agent.pubkey)}
+          >
+            <Square className="h-4 w-4" />
+            Shutdown
+          </DropdownMenuItem>
+        </>
+      ) : isActive ? (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() => onStop(agent.pubkey)}
+        >
+          <Square className="h-4 w-4" />
+          Stop
+        </DropdownMenuItem>
+      ) : (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() => onStart(agent.pubkey)}
+        >
+          <Play className="h-4 w-4" />
+          Spawn
+        </DropdownMenuItem>
+      )}
+
+      {agent.backend.type !== "provider" && onEdit ? (
+        <DropdownMenuItem onClick={onEdit}>
+          <Pencil className="h-4 w-4" />
+          Edit agent
+        </DropdownMenuItem>
+      ) : null}
+
+      {/* Opt-in promote — hidden for persona-backed agents (already reusable). */}
+      {!agent.personaId ? (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() => onSaveAsTemplate(agent)}
+        >
+          <BookmarkPlus className="h-4 w-4" />
+          Save as persona template
+        </DropdownMenuItem>
+      ) : null}
+
+      <DropdownMenuItem
+        disabled={isActionPending}
+        onClick={() => onAddToChannel(agent)}
+      >
+        <UserPlus className="h-4 w-4" />
+        Add to channel
+      </DropdownMenuItem>
+
+      <DropdownMenuItem
+        onClick={async () => {
+          await navigator.clipboard.writeText(agent.pubkey);
+          toast.success("Copied pubkey to clipboard");
+        }}
+      >
+        <Clipboard className="h-4 w-4" />
+        Copy pubkey
+      </DropdownMenuItem>
+
+      {agent.backend.type === "local" ? (
+        <DropdownMenuItem onClick={() => onOpenLogs(agent.pubkey)}>
+          <FileText className="h-4 w-4" />
+          View logs
+        </DropdownMenuItem>
+      ) : null}
+
+      {agent.backend.type === "local" ? (
+        <DropdownMenuItem
+          disabled={isActionPending}
+          onClick={() =>
+            onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
+          }
+        >
+          <Power className="h-4 w-4" />
+          {agent.startOnAppLaunch ? "Disable auto-start" : "Enable auto-start"}
+        </DropdownMenuItem>
+      ) : null}
+
+      <DropdownMenuSeparator />
+
+      <DropdownMenuItem
+        className="text-destructive focus:text-destructive"
+        disabled={isActionPending}
+        onClick={() => onDelete(agent.pubkey)}
+      >
+        <Trash2 className="h-4 w-4" />
+        Delete
+      </DropdownMenuItem>
+    </>
+  );
+}
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index 44088d490e..fbcdd36bc5 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -100,6 +100,7 @@ export function AgentsView() {
               onDeleteAgent={(pubkey) => {
                 void agents.handleDelete(pubkey);
               }}
+              onSaveAsTemplate={personas.openSaveAsTemplate}
               onSelectLogAgent={agents.setLogAgentPubkey}
               onStartAgent={(pubkey) => {
                 void agents.handleStart(pubkey);
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index cdce927572..cc3efbec4e 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -7,13 +7,8 @@ import {
   FileText,
   OctagonX,
   Pencil,
-  Play,
-  Power,
-  Square,
   Trash2,
-  UserPlus,
 } from "lucide-react";
-import { toast } from "sonner";
 
 import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore";
 import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError";
@@ -38,6 +33,7 @@ import {
   DropdownMenuTrigger,
 } from "@/shared/ui/dropdown-menu";
 import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
+import { AgentActionItems, type AgentMenuProps } from "./AgentActionItems";
 import { AgentIdentityCard } from "./AgentIdentityCard";
 import { CreateIdentityCard } from "./CreateIdentityCard";
 import { EditAgentDialog } from "./EditAgentDialog";
@@ -65,6 +61,7 @@ type UnifiedAgentsSectionProps = {
   onCreateAgent: () => void;
   onDeleteAgent: (pubkey: string) => void;
   onOpenAgentProfile?: (pubkey: string) => void;
+  onSaveAsTemplate: (agent: ManagedAgent) => void;
   onSelectLogAgent: (pubkey: string | null) => void;
   onStartAgent: (pubkey: string) => void;
   onStopAgent: (pubkey: string) => void;
@@ -110,6 +107,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     onCreateAgent,
     onDeleteAgent,
     onOpenAgentProfile,
+    onSaveAsTemplate,
     onSelectLogAgent,
     onStartAgent,
     onStopAgent,
@@ -187,6 +185,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
     onAddToChannel,
     onDelete: onDeleteAgent,
     onOpenLogs: onSelectLogAgent,
+    onSaveAsTemplate,
     onStart: onStartAgent,
     onStop: onStopAgent,
     onToggleStartOnAppLaunch,
@@ -349,16 +348,6 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
   );
 }
 
-type AgentMenuProps = {
-  isActionPending: boolean;
-  onAddToChannel: (agent: ManagedAgent) => void;
-  onDelete: (pubkey: string) => void;
-  onOpenLogs: (pubkey: string) => void;
-  onStart: (pubkey: string) => void;
-  onStop: (pubkey: string) => void;
-  onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
-};
-
 type PersonaMenuProps = {
   isActionPending: boolean;
   isPersonasPending: boolean;
@@ -614,6 +603,7 @@ function AgentActionsMenu({
   onAddToChannel,
   onDelete,
   onOpenLogs,
+  onSaveAsTemplate,
   onStart,
   onStop,
   onToggleStartOnAppLaunch,
@@ -643,6 +633,7 @@ function AgentActionsMenu({
             onAddToChannel={onAddToChannel}
             onDelete={onDelete}
             onOpenLogs={onOpenLogs}
+            onSaveAsTemplate={onSaveAsTemplate}
             onStart={onStart}
             onStop={onStop}
             onToggleStartOnAppLaunch={onToggleStartOnAppLaunch}
@@ -660,114 +651,6 @@ function AgentActionsMenu({
   );
 }
 
-function AgentActionItems({
-  agent,
-  isActionPending,
-  onAddToChannel,
-  onDelete,
-  onEdit,
-  onOpenLogs,
-  onStart,
-  onStop,
-  onToggleStartOnAppLaunch,
-}: { agent: ManagedAgent; onEdit?: () => void } & AgentMenuProps) {
-  const isActive = isManagedAgentActive(agent);
-
-  return (
-    <>
-      {agent.backend.type === "provider" ? (
-        <>
-          <DropdownMenuItem
-            disabled={isActionPending}
-            onClick={() => onStart(agent.pubkey)}
-          >
-            <Play className="h-4 w-4" />
-            {isActive ? "Redeploy" : "Deploy"}
-          </DropdownMenuItem>
-          <DropdownMenuItem
-            disabled={isActionPending}
-            onClick={() => onStop(agent.pubkey)}
-          >
-            <Square className="h-4 w-4" />
-            Shutdown
-          </DropdownMenuItem>
-        </>
-      ) : isActive ? (
-        <DropdownMenuItem
-          disabled={isActionPending}
-          onClick={() => onStop(agent.pubkey)}
-        >
-          <Square className="h-4 w-4" />
-          Stop
-        </DropdownMenuItem>
-      ) : (
-        <DropdownMenuItem
-          disabled={isActionPending}
-          onClick={() => onStart(agent.pubkey)}
-        >
-          <Play className="h-4 w-4" />
-          Spawn
-        </DropdownMenuItem>
-      )}
-
-      {agent.backend.type !== "provider" && onEdit ? (
-        <DropdownMenuItem onClick={onEdit}>
-          <Pencil className="h-4 w-4" />
-          Edit agent
-        </DropdownMenuItem>
-      ) : null}
-
-      <DropdownMenuItem
-        disabled={isActionPending}
-        onClick={() => onAddToChannel(agent)}
-      >
-        <UserPlus className="h-4 w-4" />
-        Add to channel
-      </DropdownMenuItem>
-
-      <DropdownMenuItem
-        onClick={async () => {
-          await navigator.clipboard.writeText(agent.pubkey);
-          toast.success("Copied pubkey to clipboard");
-        }}
-      >
-        <Clipboard className="h-4 w-4" />
-        Copy pubkey
-      </DropdownMenuItem>
-
-      {agent.backend.type === "local" ? (
-        <DropdownMenuItem onClick={() => onOpenLogs(agent.pubkey)}>
-          <FileText className="h-4 w-4" />
-          View logs
-        </DropdownMenuItem>
-      ) : null}
-
-      {agent.backend.type === "local" ? (
-        <DropdownMenuItem
-          disabled={isActionPending}
-          onClick={() =>
-            onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
-          }
-        >
-          <Power className="h-4 w-4" />
-          {agent.startOnAppLaunch ? "Disable auto-start" : "Enable auto-start"}
-        </DropdownMenuItem>
-      ) : null}
-
-      <DropdownMenuSeparator />
-
-      <DropdownMenuItem
-        className="text-destructive focus:text-destructive"
-        disabled={isActionPending}
-        onClick={() => onDelete(agent.pubkey)}
-      >
-        <Trash2 className="h-4 w-4" />
-        Delete
-      </DropdownMenuItem>
-    </>
-  );
-}
-
 function formatAgentModelLabel(model: string | null | undefined) {
   const trimmed = model?.trim();
   return trimmed && trimmed.length > 0 ? trimmed : "Auto";
diff --git a/desktop/src/features/agents/ui/personaDialogState.test.mjs b/desktop/src/features/agents/ui/personaDialogState.test.mjs
index 7e39b72885..1dcda856ae 100644
--- a/desktop/src/features/agents/ui/personaDialogState.test.mjs
+++ b/desktop/src/features/agents/ui/personaDialogState.test.mjs
@@ -7,8 +7,54 @@ import {
   duplicatePersonaDialogState,
   editPersonaDialogState,
   importPersonaDialogState,
+  saveAsPersonaTemplateDialogState,
 } from "./personaDialogState.ts";
 
+// Minimal ManagedAgent fixture — only the fields the save-as mapping reads.
+function makeManagedAgent(overrides = {}) {
+  return {
+    pubkey: "agentpub",
+    name: "Helper",
+    personaId: null,
+    relayUrl: "wss://relay",
+    acpCommand: "goose",
+    agentCommand: "/usr/local/bin/goose",
+    agentCommandOverride: null,
+    agentArgs: [],
+    mcpCommand: "",
+    turnTimeoutSeconds: 0,
+    idleTimeoutSeconds: null,
+    maxTurnDurationSeconds: null,
+    parallelism: 1,
+    systemPrompt: "Be helpful.",
+    model: "claude-sonnet",
+    provider: "anthropic",
+    personaOutOfDate: false,
+    personaOrphaned: false,
+    mcpToolsets: null,
+    envVars: { ANTHROPIC_API_KEY: "sk-test" },
+    ...overrides,
+  };
+}
+
+function makeRuntime(overrides = {}) {
+  return {
+    id: "goose",
+    label: "Goose",
+    avatarUrl: "",
+    availability: "available",
+    command: "goose",
+    binaryPath: "/usr/local/bin/goose",
+    defaultArgs: [],
+    mcpCommand: null,
+    installHint: "",
+    installInstructionsUrl: "",
+    canAutoInstall: false,
+    underlyingCliPath: null,
+    ...overrides,
+  };
+}
+
 test("canSubmitPersonaDialog requires a display name but not a system prompt", () => {
   // Empty system prompt is allowed: core memory is auto-injected, so the
   // persona prompt is optional. Only the display name gates submission.
@@ -258,3 +304,85 @@ test("importPersonaDialogState preserves provider=anthropic", () => {
 
   assert.equal(state.initialValues.provider, "anthropic");
 });
+
+test("saveAsPersonaTemplateDialogState carries agent config into a create draft", () => {
+  const state = saveAsPersonaTemplateDialogState(makeManagedAgent(), [
+    makeRuntime(),
+  ]);
+
+  assert.equal(state.title, "Save as persona template");
+  assert.equal(state.submitLabel, "Save as persona template");
+  assert.equal(state.description, "Reuse this setup to create more agents.");
+  assert.deepEqual(state.initialValues, {
+    displayName: "Helper",
+    avatarUrl: "",
+    systemPrompt: "Be helpful.",
+    // Reverse-mapped from agentCommand basename → matching runtime id.
+    runtime: "goose",
+    model: "claude-sonnet",
+    provider: "anthropic",
+    // Persona-only field starts empty; the user fills it in the dialog.
+    namePool: [],
+    envVars: { ANTHROPIC_API_KEY: "sk-test" },
+  });
+});
+
+test("saveAsPersonaTemplateDialogState reverse-maps the runtime by command basename", () => {
+  // Agent's resolved command is an absolute path; the runtime exposes a bare
+  // command. commandsMatch normalizes on basename, so they should still pair.
+  const state = saveAsPersonaTemplateDialogState(
+    makeManagedAgent({ agentCommand: "/opt/homebrew/bin/goose" }),
+    [makeRuntime({ id: "goose-runtime", command: "goose" })],
+  );
+
+  assert.equal(state.initialValues.runtime, "goose-runtime");
+});
+
+test("saveAsPersonaTemplateDialogState falls back to undefined runtime when none match", () => {
+  // No runtime matches the agent command, or runtimes not loaded yet — the
+  // dialog then uses its own default-runtime behavior.
+  const noMatch = saveAsPersonaTemplateDialogState(
+    makeManagedAgent({ agentCommand: "claude-code-acp" }),
+    [makeRuntime({ command: "goose" })],
+  );
+  assert.equal(noMatch.initialValues.runtime, undefined);
+
+  const noRuntimes = saveAsPersonaTemplateDialogState(makeManagedAgent(), []);
+  assert.equal(noRuntimes.initialValues.runtime, undefined);
+});
+
+test("saveAsPersonaTemplateDialogState skips runtimes with a null command", () => {
+  // Catalog entries can be unavailable (command: null). Those must not throw
+  // and must not match — only resolvable commands participate in the map.
+  const state = saveAsPersonaTemplateDialogState(makeManagedAgent(), [
+    makeRuntime({ id: "uninstalled", command: null, availability: "missing" }),
+    makeRuntime({ id: "goose", command: "goose" }),
+  ]);
+
+  assert.equal(state.initialValues.runtime, "goose");
+});
+
+test("saveAsPersonaTemplateDialogState maps a null provider/model/systemPrompt to undefined/empty", () => {
+  const state = saveAsPersonaTemplateDialogState(
+    makeManagedAgent({ provider: null, model: null, systemPrompt: null }),
+    [],
+  );
+
+  assert.equal(state.initialValues.provider, undefined);
+  assert.equal(state.initialValues.model, undefined);
+  assert.equal(state.initialValues.systemPrompt, "");
+});
+
+test("default managed-agent create stays persona-less (no personaId set)", () => {
+  // Part 1 regression guard. A default agent create must not carry a
+  // personaId — that linkage only exists when a persona/template is chosen.
+  // The save-as flow promotes an existing agent INTO a template; it never
+  // back-fills personaId onto the source agent.
+  const agent = makeManagedAgent();
+  assert.equal(agent.personaId, null);
+
+  // The promote produces a CreatePersonaInput (no agent personaId mutation),
+  // and a persona-create draft has no personaId field at all.
+  const state = saveAsPersonaTemplateDialogState(agent, []);
+  assert.equal("personaId" in state.initialValues, false);
+});
diff --git a/desktop/src/features/agents/ui/personaDialogState.ts b/desktop/src/features/agents/ui/personaDialogState.ts
index 0a8c2a9140..792121c9e5 100644
--- a/desktop/src/features/agents/ui/personaDialogState.ts
+++ b/desktop/src/features/agents/ui/personaDialogState.ts
@@ -1,9 +1,12 @@
 import type { ParsePersonaFilesResult } from "@/shared/api/tauriPersonas";
 import type {
+  AcpRuntimeCatalogEntry,
   AgentPersona,
   CreatePersonaInput,
+  ManagedAgent,
   UpdatePersonaInput,
 } from "@/shared/api/types";
+import { commandsMatch } from "@/features/agents/agentReuse";
 
 export type PersonaDialogState = {
   description: string;
@@ -70,6 +73,58 @@ export function duplicatePersonaDialogState(
   };
 }
 
+/**
+ * Reverse-map a managed agent's resolved harness command back to an ACP
+ * runtime ID, so the persona dialog can pre-select the matching runtime.
+ * Returns `undefined` when no runtime matches (or none are loaded yet) — the
+ * dialog then falls back to its default-runtime behavior.
+ */
+function runtimeIdForAgentCommand(
+  agentCommand: string,
+  runtimes: readonly AcpRuntimeCatalogEntry[],
+): string | undefined {
+  const match = runtimes.find(
+    (runtime) =>
+      runtime.command !== null && commandsMatch(runtime.command, agentCommand),
+  );
+  return match?.id;
+}
+
+/**
+ * Dialog state for the opt-in "Save as persona template" action on an existing
+ * agent. Prefills the persona editor from the agent so the user reviews and
+ * confirms before a persona template is created — nothing is minted silently.
+ *
+ * Near-lossless promote: name, system prompt, model, provider, and env vars
+ * copy straight across; the harness command reverse-maps to a runtime ID.
+ * `namePool` is persona-only and starts empty — the user can fill it in the
+ * same dialog (it's how a template bulk-adds bots later).
+ *
+ * Note: "persona template" is the UI name for what the backend calls a
+ * `persona` (kind:30175). This builder produces a backend `CreatePersonaInput`.
+ */
+export function saveAsPersonaTemplateDialogState(
+  agent: ManagedAgent,
+  runtimes: readonly AcpRuntimeCatalogEntry[],
+): PersonaDialogState {
+  return {
+    title: "Save as persona template",
+    description: "Reuse this setup to create more agents.",
+    submitLabel: "Save as persona template",
+    initialValues: {
+      displayName: agent.name,
+      avatarUrl: "",
+      systemPrompt: agent.systemPrompt ?? "",
+      runtime: runtimeIdForAgentCommand(agent.agentCommand, runtimes),
+      model: agent.model ?? undefined,
+      provider: agent.provider ?? undefined,
+      // namePool is persona-only; start empty so the user fills it here.
+      namePool: [],
+      envVars: agent.envVars ?? {},
+    },
+  };
+}
+
 export function editPersonaDialogState(
   persona: AgentPersona,
 ): PersonaDialogState {
diff --git a/desktop/src/features/agents/ui/personaLibraryCopy.ts b/desktop/src/features/agents/ui/personaLibraryCopy.ts
index 9ea9fc2584..bd1e770262 100644
--- a/desktop/src/features/agents/ui/personaLibraryCopy.ts
+++ b/desktop/src/features/agents/ui/personaLibraryCopy.ts
@@ -1,3 +1,7 @@
+// Naming boundary: user-facing copy says "persona template" (the reusable
+// setup users save and reuse), while the backend type/storage stays `persona`
+// (kind:30175, builtin:* ids, .persona.md). Do NOT rename backend symbols to
+// match the UI — the two names map across this boundary intentionally.
 export const personaLibraryCopy = {
   title: "My agents",
   description:
diff --git a/desktop/src/features/agents/ui/usePersonaActions.ts b/desktop/src/features/agents/ui/usePersonaActions.ts
index 76fd78b3c6..369195a516 100644
--- a/desktop/src/features/agents/ui/usePersonaActions.ts
+++ b/desktop/src/features/agents/ui/usePersonaActions.ts
@@ -20,6 +20,7 @@ import { isSingleItemFile } from "@/shared/lib/fileMagic";
 import type {
   AgentPersona,
   CreatePersonaInput,
+  ManagedAgent,
   UpdatePersonaInput,
 } from "@/shared/api/types";
 import {
@@ -27,6 +28,7 @@ import {
   duplicatePersonaDialogState,
   editPersonaDialogState,
   importPersonaDialogState,
+  saveAsPersonaTemplateDialogState,
   type PersonaDialogState,
 } from "./personaDialogState";
 import { usePersonaImportActions } from "./usePersonaImportActions";
@@ -203,6 +205,17 @@ export function usePersonaActions() {
     setPersonaDialogState(duplicatePersonaDialogState(persona));
   }
 
+  function openSaveAsTemplate(agent: ManagedAgent) {
+    clearFeedback("library");
+    setShouldLoadAcpRuntimes(true);
+    // Reverse-map against whatever runtimes are already cached; the dialog
+    // refines once the lazy query resolves. Best-effort — falls back to the
+    // default runtime when no match is available yet.
+    setPersonaDialogState(
+      saveAsPersonaTemplateDialogState(agent, acpRuntimesQuery.data ?? []),
+    );
+  }
+
   function openCatalog() {
     clearFeedback("catalog");
     setIsCatalogDialogOpen(true);
@@ -252,6 +265,7 @@ export function usePersonaActions() {
     openCreate,
     openEdit,
     openDuplicate,
+    openSaveAsTemplate,
     openCatalog,
     openDelete,
     clearFeedback,
diff --git a/desktop/src/features/agents/ui/useSaveAsPersonaTemplate.ts b/desktop/src/features/agents/ui/useSaveAsPersonaTemplate.ts
new file mode 100644
index 0000000000..716359e4f9
--- /dev/null
+++ b/desktop/src/features/agents/ui/useSaveAsPersonaTemplate.ts
@@ -0,0 +1,90 @@
+import * as React from "react";
+import { toast } from "sonner";
+
+import {
+  useAcpRuntimesQuery,
+  useCreatePersonaMutation,
+} from "@/features/agents/hooks";
+import type {
+  CreatePersonaInput,
+  ManagedAgent,
+  UpdatePersonaInput,
+} from "@/shared/api/types";
+import {
+  saveAsPersonaTemplateDialogState,
+  type PersonaDialogState,
+} from "./personaDialogState";
+
+/**
+ * Self-contained "Save as persona template" flow for surfaces outside the
+ * Agents page (e.g. the sidebar agent profile) that don't already host
+ * `usePersonaActions`. Opens the shared `PersonaDialog` prefilled from an
+ * agent and creates a backend persona on submit — no new backend or IPC.
+ *
+ * "Persona template" is the UI name for what the backend calls a `persona`
+ * (kind:30175); this hook produces a `CreatePersonaInput`.
+ */
+export function useSaveAsPersonaTemplate() {
+  const [dialogState, setDialogState] =
+    React.useState<PersonaDialogState | null>(null);
+  // Only fetch runtimes once the user actually opens the dialog.
+  const [shouldLoadRuntimes, setShouldLoadRuntimes] = React.useState(false);
+  const acpRuntimesQuery = useAcpRuntimesQuery({ enabled: shouldLoadRuntimes });
+  const createPersonaMutation = useCreatePersonaMutation();
+
+  const open = React.useCallback(
+    (agent: ManagedAgent) => {
+      setShouldLoadRuntimes(true);
+      setDialogState(
+        saveAsPersonaTemplateDialogState(agent, acpRuntimesQuery.data ?? []),
+      );
+    },
+    [acpRuntimesQuery.data],
+  );
+
+  const close = React.useCallback(() => {
+    setDialogState(null);
+  }, []);
+
+  const handleSubmit = React.useCallback(
+    async (input: CreatePersonaInput | UpdatePersonaInput) => {
+      // The save-as flow only ever produces a create input.
+      if ("id" in input) return;
+      try {
+        await createPersonaMutation.mutateAsync(input);
+        toast.success(`Saved ${input.displayName} as a persona template.`);
+        setDialogState(null);
+      } catch (error) {
+        toast.error(
+          error instanceof Error
+            ? error.message
+            : "Failed to save persona template.",
+        );
+      }
+    },
+    [createPersonaMutation],
+  );
+
+  return {
+    open,
+    dialogState,
+    dialogProps: {
+      open: dialogState !== null,
+      title: dialogState?.title ?? "",
+      description: dialogState?.description ?? "",
+      submitLabel: dialogState?.submitLabel ?? "",
+      initialValues: dialogState?.initialValues ?? null,
+      error:
+        createPersonaMutation.error instanceof Error
+          ? createPersonaMutation.error
+          : null,
+      isPending: createPersonaMutation.isPending,
+      runtimes: acpRuntimesQuery.data ?? [],
+      runtimesLoading: acpRuntimesQuery.isLoading,
+      onOpenChange: (next: boolean) => {
+        if (!next) close();
+      },
+      onSubmit: handleSubmit,
+    },
+  };
+}
diff --git a/desktop/src/features/profile/ui/ProfileQuickActions.tsx b/desktop/src/features/profile/ui/ProfileQuickActions.tsx
new file mode 100644
index 0000000000..267354481a
--- /dev/null
+++ b/desktop/src/features/profile/ui/ProfileQuickActions.tsx
@@ -0,0 +1,88 @@
+import * as React from "react";
+import type { LucideIcon } from "lucide-react";
+
+import { cn } from "@/shared/lib/cn";
+
+/**
+ * A single circular quick action (icon + label) in the profile summary's
+ * action row — e.g. Follow / Message / Edit.
+ */
+export function ProfileQuickAction({
+  active,
+  disabled,
+  icon: Icon,
+  label,
+  onClick,
+  testId,
+}: {
+  active?: boolean;
+  disabled?: boolean;
+  icon: LucideIcon;
+  label: string;
+  onClick: () => void;
+  testId?: string;
+}) {
+  return (
+    <button
+      className="flex flex-col items-center gap-2 disabled:cursor-not-allowed disabled:opacity-50"
+      data-testid={testId}
+      disabled={disabled}
+      onClick={onClick}
+      type="button"
+    >
+      <span
+        className={cn(
+          "flex h-14 w-14 items-center justify-center rounded-full transition-colors",
+          active
+            ? "bg-foreground text-background hover:bg-foreground/90"
+            : "bg-muted/60 text-foreground hover:bg-muted/80",
+        )}
+      >
+        <Icon className="h-4 w-4" />
+      </span>
+      <span
+        className={cn(
+          "text-xs",
+          active ? "text-foreground" : "text-muted-foreground",
+        )}
+      >
+        {label}
+      </span>
+    </button>
+  );
+}
+
+/**
+ * A quick action styled to match `ProfileQuickAction` but rendered as a
+ * `forwardRef` button so it can be a Radix `DropdownMenuTrigger asChild`
+ * (which clones the child and injects a ref + `aria-*`/event props). Used for
+ * the overflow `⋮` that hosts actions like "Save as persona template".
+ */
+export const ProfileQuickActionTrigger = React.forwardRef<
+  HTMLButtonElement,
+  {
+    ariaLabel: string;
+    icon: LucideIcon;
+    label: string;
+    testId?: string;
+  } & React.ComponentPropsWithoutRef<"button">
+>(function ProfileQuickActionTrigger(
+  { ariaLabel, icon: Icon, label, testId, ...props },
+  ref,
+) {
+  return (
+    <button
+      aria-label={ariaLabel}
+      className="flex flex-col items-center gap-2 disabled:cursor-not-allowed disabled:opacity-50"
+      data-testid={testId}
+      ref={ref}
+      type="button"
+      {...props}
+    >
+      <span className="flex h-14 w-14 items-center justify-center rounded-full bg-muted/60 text-foreground transition-colors hover:bg-muted/80">
+        <Icon className="h-4 w-4" />
+      </span>
+      <span className="text-xs text-muted-foreground">{label}</span>
+    </button>
+  );
+});
diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx
index 335202c224..d5a6c67cad 100644
--- a/desktop/src/features/profile/ui/UserProfilePanel.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx
@@ -14,6 +14,8 @@ import {
 import { useActiveAgentTurnsBridge } from "@/features/agents/activeAgentTurnsStore";
 import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore";
 import { EditAgentDialog } from "@/features/agents/ui/EditAgentDialog";
+import { PersonaDialog } from "@/features/agents/ui/PersonaDialog";
+import { useSaveAsPersonaTemplate } from "@/features/agents/ui/useSaveAsPersonaTemplate";
 import { useChannelsQuery } from "@/features/channels/hooks";
 import { usePresenceQuery } from "@/features/presence/hooks";
 import {
@@ -278,6 +280,17 @@ export function UserProfilePanel({
     setEditAgentOpen(true);
   }, []);
 
+  // "Save as persona template" flow for the sidebar profile. Self-contained
+  // (own dialog + create mutation) so it works outside the Agents page, which
+  // is where `usePersonaActions` lives. Gated on `canEditAgent` below — only a
+  // managed agent the viewer owns can be promoted.
+  const saveAsTemplate = useSaveAsPersonaTemplate();
+  const handleSaveAsTemplate = React.useCallback(() => {
+    if (managedAgent) {
+      saveAsTemplate.open(managedAgent);
+    }
+  }, [managedAgent, saveAsTemplate]);
+
   const handleOpenActivity = React.useCallback(() => {
     onClose();
     onOpenAgentSession?.(pubkey);
@@ -389,6 +402,7 @@ export function UserProfilePanel({
           handleEditAgent={handleEditAgent}
           handleMessage={handleMessage}
           handleOpenActivity={handleOpenActivity}
+          handleSaveAsTemplate={canEditAgent ? handleSaveAsTemplate : undefined}
           isBot={isBot}
           isFollowing={isFollowing}
           isOwner={viewerIsOwner}
@@ -441,6 +455,11 @@ export function UserProfilePanel({
       />
     ) : null;
 
+  // Render unconditionally — the dialog stays closed until `open()` seeds it.
+  const saveAsTemplateDialog = canEditAgent ? (
+    <PersonaDialog {...saveAsTemplate.dialogProps} />
+  ) : null;
+
   if (isSplitLayout) {
     return (
       <>
@@ -452,6 +471,7 @@ export function UserProfilePanel({
           {profileBody}
         </div>
         {editAgentDialog}
+        {saveAsTemplateDialog}
       </>
     );
   }
@@ -517,6 +537,7 @@ export function UserProfilePanel({
         {profileBody}
       </aside>
       {editAgentDialog}
+      {saveAsTemplateDialog}
     </>
   );
 }
diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
index 42a3739c30..67f1f7c989 100644
--- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
@@ -3,12 +3,14 @@ import type { LucideIcon } from "lucide-react";
 import {
   Activity,
   ArrowUpRight,
+  BookmarkPlus,
   Brain,
   ChevronDown,
   ChevronRight,
   ChevronUp,
   Copy,
   Cpu,
+  Ellipsis,
   Fingerprint,
   Hash,
   MessageSquare,
@@ -42,7 +44,17 @@ import { useFeatureEnabled } from "@/shared/features";
 import { cn } from "@/shared/lib/cn";
 import { useNow } from "@/shared/lib/useNow";
 import { Badge } from "@/shared/ui/badge";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/shared/ui/dropdown-menu";
 import { UserAvatar } from "@/shared/ui/UserAvatar";
+import {
+  ProfileQuickAction,
+  ProfileQuickActionTrigger,
+} from "./ProfileQuickActions";
 
 const RUNTIME_LABELS: Record<string, string> = {
   goose: "Goose",
@@ -73,6 +85,7 @@ export type ProfileSummaryViewProps = {
   handleEditAgent: () => void;
   handleMessage: () => void;
   handleOpenActivity: () => void;
+  handleSaveAsTemplate?: () => void;
   isBot: boolean;
   isFollowing: boolean;
   isOwner: boolean | undefined;
@@ -108,6 +121,7 @@ export function ProfileSummaryView({
   handleEditAgent,
   handleMessage,
   handleOpenActivity,
+  handleSaveAsTemplate,
   isBot,
   isFollowing,
   isOwner,
@@ -176,6 +190,7 @@ export function ProfileSummaryView({
           canEditAgent={canEditAgent}
           followMutation={followMutation}
           onEditAgent={handleEditAgent}
+          onSaveAsTemplate={handleSaveAsTemplate}
           isFollowing={isFollowing}
           onMessage={onOpenDm ? handleMessage : undefined}
           pubkey={pubkey}
@@ -430,6 +445,7 @@ function ProfilePrimaryActions({
   followMutation,
   isFollowing,
   onEditAgent,
+  onSaveAsTemplate,
   onMessage,
   pubkey,
   unfollowMutation,
@@ -438,6 +454,7 @@ function ProfilePrimaryActions({
   followMutation: ReturnType<typeof useFollowMutation>;
   isFollowing: boolean;
   onEditAgent: () => void;
+  onSaveAsTemplate?: () => void;
   onMessage?: () => void;
   pubkey: string;
   unfollowMutation: ReturnType<typeof useUnfollowMutation>;
@@ -454,6 +471,11 @@ function ProfilePrimaryActions({
     });
   };
 
+  // Keep the quick-action row lean: overflow actions (e.g. "Save as persona
+  // template") live behind a small ⋮ rather than crowding the row with a
+  // dedicated button.
+  const showOverflow = canEditAgent && Boolean(onSaveAsTemplate);
+
   return (
     <div className="flex items-start justify-center gap-8">
       {showFollowAction ? (
@@ -481,57 +503,34 @@ function ProfilePrimaryActions({
           testId="user-profile-edit-agent"
         />
       ) : null}
+      {showOverflow ? (
+        <DropdownMenu modal={false}>
+          <DropdownMenuTrigger asChild>
+            <ProfileQuickActionTrigger
+              ariaLabel="More agent actions"
+              icon={Ellipsis}
+              label="More"
+              testId="user-profile-more-actions"
+            />
+          </DropdownMenuTrigger>
+          <DropdownMenuContent
+            align="end"
+            onCloseAutoFocus={(event) => event.preventDefault()}
+          >
+            <DropdownMenuItem
+              onClick={onSaveAsTemplate}
+              data-testid="user-profile-save-as-persona-template"
+            >
+              <BookmarkPlus className="h-4 w-4" />
+              Save as persona template
+            </DropdownMenuItem>
+          </DropdownMenuContent>
+        </DropdownMenu>
+      ) : null}
     </div>
   );
 }
 
-function ProfileQuickAction({
-  active,
-  disabled,
-  icon: Icon,
-  label,
-  onClick,
-  testId,
-}: {
-  active?: boolean;
-  disabled?: boolean;
-  icon: LucideIcon;
-  label: string;
-  onClick: () => void;
-  testId?: string;
-}) {
-  return (
-    <button
-      className="flex flex-col items-center gap-2 disabled:cursor-not-allowed disabled:opacity-50"
-      data-testid={testId}
-      disabled={disabled}
-      onClick={onClick}
-      type="button"
-    >
-      <span
-        className={cn(
-          "flex h-14 w-14 items-center justify-center rounded-full transition-colors",
-          active
-            ? "bg-foreground text-background hover:bg-foreground/90"
-            : "bg-muted/60 text-foreground hover:bg-muted/80",
-        )}
-      >
-        <Icon className="h-4 w-4" />
-      </span>
-      <span
-        className={cn(
-          "text-xs",
-          active ? "text-foreground" : "text-muted-foreground",
-        )}
-      >
-        {label}
-      </span>
-    </button>
-  );
-}
-
-// ── Field rows ───────────────────────────────────────────────────────────────
-
 type ProfileField = {
   copyValue?: string;
   /**
