Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 149 additions & 15 deletions desktop/src-tauri/src/managed_agents/personas.rs

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions desktop/src-tauri/src/managed_agents/personas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ fn merge_personas_adds_missing_built_ins() {
assert!(changed);
assert_eq!(records.len(), BUILT_IN_PERSONAS.len());
assert!(records.iter().all(|record| record.is_builtin));
assert!(records.iter().all(|record| record.is_active));
let display_names: Vec<&str> = records
.iter()
.map(|record| record.display_name.as_str())
.collect();
assert_eq!(display_names, vec!["Fizz"]);
assert_eq!(
display_names,
vec!["Fizz", "Angelica", "Bart", "Chucky", "Marge", "Ned", "Tommy"]
);
let active_ids: Vec<&str> = records
.iter()
.filter(|record| record.is_active)
.map(|record| record.id.as_str())
.collect();
assert_eq!(active_ids, vec!["builtin:fizz"]);
}

#[test]
Expand Down Expand Up @@ -201,7 +209,7 @@ fn ensure_persona_is_active_rejects_inactive_personas() {

assert_eq!(
err,
"Fizz is not in My Agents. Choose it from Persona Catalog first."
"Fizz is not in My Agents. Choose it from Agent Catalog first."
);
}

Expand Down
23 changes: 21 additions & 2 deletions desktop/src/features/agents/lib/catalog.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import test from "node:test";
import {
getCatalogPersonas,
getCatalogSelectionState,
getLibraryPersonas,
getPersonaLabelsById,
getPersonaLibraryState,
isCatalogPersonaSelected,
Expand Down Expand Up @@ -80,7 +81,7 @@ test("getCatalogPersonas keeps chooser order stable when selection changes", ()
);
});

test("isCatalogPersonaSelected only treats active built-ins as selected", () => {
test("isCatalogPersonaSelected treats active catalog personas as selected", () => {
assert.equal(
isCatalogPersonaSelected(
createPersona("builtin:fizz", "Fizz", {
Expand All @@ -101,7 +102,7 @@ test("isCatalogPersonaSelected only treats active built-ins as selected", () =>
);
assert.equal(
isCatalogPersonaSelected(createPersona("custom:builder", "Builder")),
false,
true,
);
});

Expand Down Expand Up @@ -135,3 +136,21 @@ test("getPersonaLibraryState keeps the working library and full catalog in one p
);
assert.equal(state.personaLabelsById["builtin:fizz"], "Fizz");
});

test("getLibraryPersonas keeps active custom personas even when catalog entries are similar", () => {
const avatarUrl = "https://example.test/marge.png";
const personas = [
createPersona("builtin:marge", "Marge", {
avatarUrl,
isBuiltIn: true,
isActive: false,
}),
createPersona("custom:marge", "Marge", { avatarUrl, isActive: true }),
createPersona("custom:builder", "Builder", { isActive: true }),
];

assert.deepEqual(
getLibraryPersonas(personas).map((persona) => persona.id),
["custom:marge", "custom:builder"],
);
});
33 changes: 27 additions & 6 deletions desktop/src/features/agents/lib/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,37 @@ export function getActivePersonas(personas: readonly AgentPersona[]) {
return personas.filter(isPersonaActive);
}

export function getCatalogPersonas(personas: readonly AgentPersona[]) {
export function getLibraryPersonas(personas: readonly AgentPersona[]) {
return getActivePersonas(personas);
}

export function isPersonaVisibleInCatalog(
persona: AgentPersona,
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
) {
return persona.isBuiltIn || sharedCatalogPersonaIds.has(persona.id);
}

export function getCatalogPersonas(
personas: readonly AgentPersona[],
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
) {
return personas
.filter((persona) => persona.isBuiltIn)
.filter((persona) =>
isPersonaVisibleInCatalog(persona, sharedCatalogPersonaIds),
)
.sort((left, right) => left.displayName.localeCompare(right.displayName));
}

export function isCatalogPersonaSelected(persona: AgentPersona) {
return persona.isBuiltIn && persona.isActive;
return persona.isActive;
}

export function getCatalogSelectionState(
personas: readonly AgentPersona[],
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
): CatalogSelectionState {
const catalogPersonas = getCatalogPersonas(personas);
const catalogPersonas = getCatalogPersonas(personas, sharedCatalogPersonaIds);

return {
catalogPersonas,
Expand All @@ -52,9 +69,13 @@ export function getPersonaLabelsById(personas: readonly AgentPersona[]) {

export function getPersonaLibraryState(
personas: readonly AgentPersona[],
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
): PersonaLibraryState {
const libraryPersonas = getActivePersonas(personas);
const { catalogPersonas } = getCatalogSelectionState(personas);
const libraryPersonas = getLibraryPersonas(personas);
const { catalogPersonas } = getCatalogSelectionState(
personas,
sharedCatalogPersonaIds,
);

return {
catalogPersonas,
Expand Down
32 changes: 31 additions & 1 deletion desktop/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PersonaCatalogDialog } from "./PersonaCatalogDialog";
import { PersonaDialog } from "./PersonaDialog";
import { PersonaDeleteDialog } from "./PersonaDeleteDialog";
import { PersonaImportUpdateDialog } from "./PersonaImportUpdateDialog";
import { PersonaShareDialog } from "./PersonaShareDialog";
import { RelayDirectorySection } from "./RelayDirectorySection";
import { SecretRevealDialog } from "./SecretRevealDialog";
import { TeamDeleteDialog } from "./TeamDeleteDialog";
Expand Down Expand Up @@ -138,7 +139,7 @@ export function AgentsView() {
onChooseCatalog={personas.openCatalog}
onDuplicatePersona={personas.openDuplicate}
onEditPersona={personas.openEdit}
onExportPersona={personas.handleExport}
onSharePersona={personas.openShare}
onDeactivatePersona={(persona) => {
void personas.handleSetActive(persona, false, "library");
}}
Expand Down Expand Up @@ -268,6 +269,35 @@ export function AgentsView() {
persona={personas.personaToDelete}
/>
) : null}
{personas.personaToShare ? (
<PersonaShareDialog
isCatalogVisible={
personas.personaToShare.isBuiltIn ||
personas.sharedCatalogPersonaIdSet.has(personas.personaToShare.id)
}
isPending={personas.isPending}
onCatalogVisibilityChange={(visible) => {
if (personas.personaToShare) {
personas.setPersonaCatalogVisibility(
personas.personaToShare,
visible,
);
}
}}
onExport={() => {
if (personas.personaToShare) {
personas.handleExport(personas.personaToShare);
}
}}
onOpenChange={(open) => {
if (!open) {
personas.setPersonaToShare(null);
}
}}
open={personas.personaToShare !== null}
persona={personas.personaToShare}
/>
) : null}
{personas.isCatalogDialogOpen ? (
<PersonaCatalogDialog
error={
Expand Down
39 changes: 19 additions & 20 deletions desktop/src/features/agents/ui/PersonaActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CopyPlus, Download, Ellipsis, Pencil, Trash2 } from "lucide-react";
import { CopyPlus, Ellipsis, Pencil, Share2, Trash2 } from "lucide-react";

import type { AgentPersona } from "@/shared/api/types";
import {
Expand All @@ -15,7 +15,7 @@ export function PersonaActionsMenu({
persona,
onDuplicate,
onEdit,
onExport,
onShare,
onDeactivate,
onDelete,
}: {
Expand All @@ -24,11 +24,12 @@ export function PersonaActionsMenu({
persona: AgentPersona;
onDuplicate: (persona: AgentPersona) => void;
onEdit: (persona: AgentPersona) => void;
onExport: (persona: AgentPersona) => void;
onShare: (persona: AgentPersona) => void;
onDeactivate: (persona: AgentPersona) => void;
onDelete: (persona: AgentPersona) => void;
}) {
const disabled = isActionPending || isPending;
const canEdit = !persona.isBuiltIn && !persona.sourceTeam;

return (
<DropdownMenu modal={false}>
Expand All @@ -45,7 +46,11 @@ export function PersonaActionsMenu({
align="end"
onCloseAutoFocus={(event) => event.preventDefault()}
>
{!persona.isBuiltIn ? (
<DropdownMenuItem disabled={disabled} onClick={() => onShare(persona)}>
<Share2 className="h-4 w-4" />
Share
</DropdownMenuItem>
Comment thread
klopez4212 marked this conversation as resolved.
{canEdit ? (
<DropdownMenuItem disabled={disabled} onClick={() => onEdit(persona)}>
<Pencil className="h-4 w-4" />
Edit
Expand All @@ -58,21 +63,8 @@ export function PersonaActionsMenu({
<CopyPlus className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem disabled={disabled} onClick={() => onExport(persona)}>
<Download className="h-4 w-4" />
Export
</DropdownMenuItem>
<DropdownMenuSeparator />
{persona.isBuiltIn ? (
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={disabled}
onClick={() => onDeactivate(persona)}
>
<Trash2 className="h-4 w-4" />
Remove from My Agents
</DropdownMenuItem>
) : persona.sourceTeam ? (
{persona.sourceTeam ? (
<DropdownMenuItem disabled>
<Trash2 className="h-4 w-4" />
Managed by team
Expand All @@ -81,10 +73,17 @@ export function PersonaActionsMenu({
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={disabled}
onClick={() => onDelete(persona)}
onClick={() => {
if (persona.isBuiltIn) {
onDeactivate(persona);
return;
}

onDelete(persona);
}}
>
<Trash2 className="h-4 w-4" />
Delete
Remove from My Agents
</DropdownMenuItem>
)}
</DropdownMenuContent>
Expand Down
14 changes: 14 additions & 0 deletions desktop/src/features/agents/ui/PersonaAddedBy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { cn } from "@/shared/lib/cn";

type PersonaAddedByProps = {
className?: string;
};

export function PersonaAddedBy({ className }: PersonaAddedByProps) {
return (
<p className={cn("truncate text-xs leading-tight", className)}>
<span className="text-muted-foreground/55">Added by</span>{" "}
<span className="text-muted-foreground">You</span>
</p>
);
}
Loading