Skip to content

Commit d9e848b

Browse files
committed
Add agent catalog modal
1 parent c384179 commit d9e848b

14 files changed

Lines changed: 1046 additions & 156 deletions

File tree

desktop/src-tauri/src/managed_agents/personas.rs

Lines changed: 149 additions & 15 deletions
Large diffs are not rendered by default.

desktop/src-tauri/src/managed_agents/personas/tests.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,20 @@ fn merge_personas_adds_missing_built_ins() {
3333
assert!(changed);
3434
assert_eq!(records.len(), BUILT_IN_PERSONAS.len());
3535
assert!(records.iter().all(|record| record.is_builtin));
36-
assert!(records.iter().all(|record| record.is_active));
3736
let display_names: Vec<&str> = records
3837
.iter()
3938
.map(|record| record.display_name.as_str())
4039
.collect();
41-
assert_eq!(display_names, vec!["Fizz"]);
40+
assert_eq!(
41+
display_names,
42+
vec!["Fizz", "Angelica", "Bart", "Chucky", "Marge", "Ned", "Tommy"]
43+
);
44+
let active_ids: Vec<&str> = records
45+
.iter()
46+
.filter(|record| record.is_active)
47+
.map(|record| record.id.as_str())
48+
.collect();
49+
assert_eq!(active_ids, vec!["builtin:fizz"]);
4250
}
4351

4452
#[test]
@@ -201,7 +209,7 @@ fn ensure_persona_is_active_rejects_inactive_personas() {
201209

202210
assert_eq!(
203211
err,
204-
"Fizz is not in My Agents. Choose it from Persona Catalog first."
212+
"Fizz is not in My Agents. Choose it from Agent Catalog first."
205213
);
206214
}
207215

desktop/src/features/agents/lib/catalog.test.mjs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import test from "node:test";
44
import {
55
getCatalogPersonas,
66
getCatalogSelectionState,
7+
getLibraryPersonas,
78
getPersonaLabelsById,
89
getPersonaLibraryState,
910
isCatalogPersonaSelected,
@@ -80,7 +81,7 @@ test("getCatalogPersonas keeps chooser order stable when selection changes", ()
8081
);
8182
});
8283

83-
test("isCatalogPersonaSelected only treats active built-ins as selected", () => {
84+
test("isCatalogPersonaSelected treats active catalog personas as selected", () => {
8485
assert.equal(
8586
isCatalogPersonaSelected(
8687
createPersona("builtin:fizz", "Fizz", {
@@ -101,7 +102,7 @@ test("isCatalogPersonaSelected only treats active built-ins as selected", () =>
101102
);
102103
assert.equal(
103104
isCatalogPersonaSelected(createPersona("custom:builder", "Builder")),
104-
false,
105+
true,
105106
);
106107
});
107108

@@ -135,3 +136,21 @@ test("getPersonaLibraryState keeps the working library and full catalog in one p
135136
);
136137
assert.equal(state.personaLabelsById["builtin:fizz"], "Fizz");
137138
});
139+
140+
test("getLibraryPersonas keeps active custom personas even when catalog entries are similar", () => {
141+
const avatarUrl = "https://example.test/marge.png";
142+
const personas = [
143+
createPersona("builtin:marge", "Marge", {
144+
avatarUrl,
145+
isBuiltIn: true,
146+
isActive: false,
147+
}),
148+
createPersona("custom:marge", "Marge", { avatarUrl, isActive: true }),
149+
createPersona("custom:builder", "Builder", { isActive: true }),
150+
];
151+
152+
assert.deepEqual(
153+
getLibraryPersonas(personas).map((persona) => persona.id),
154+
["custom:marge", "custom:builder"],
155+
);
156+
});

desktop/src/features/agents/lib/catalog.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,37 @@ export function getActivePersonas(personas: readonly AgentPersona[]) {
2020
return personas.filter(isPersonaActive);
2121
}
2222

23-
export function getCatalogPersonas(personas: readonly AgentPersona[]) {
23+
export function getLibraryPersonas(personas: readonly AgentPersona[]) {
24+
return getActivePersonas(personas);
25+
}
26+
27+
export function isPersonaVisibleInCatalog(
28+
persona: AgentPersona,
29+
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
30+
) {
31+
return persona.isBuiltIn || sharedCatalogPersonaIds.has(persona.id);
32+
}
33+
34+
export function getCatalogPersonas(
35+
personas: readonly AgentPersona[],
36+
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
37+
) {
2438
return personas
25-
.filter((persona) => persona.isBuiltIn)
39+
.filter((persona) =>
40+
isPersonaVisibleInCatalog(persona, sharedCatalogPersonaIds),
41+
)
2642
.sort((left, right) => left.displayName.localeCompare(right.displayName));
2743
}
2844

2945
export function isCatalogPersonaSelected(persona: AgentPersona) {
30-
return persona.isBuiltIn && persona.isActive;
46+
return persona.isActive;
3147
}
3248

3349
export function getCatalogSelectionState(
3450
personas: readonly AgentPersona[],
51+
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
3552
): CatalogSelectionState {
36-
const catalogPersonas = getCatalogPersonas(personas);
53+
const catalogPersonas = getCatalogPersonas(personas, sharedCatalogPersonaIds);
3754

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

5370
export function getPersonaLibraryState(
5471
personas: readonly AgentPersona[],
72+
sharedCatalogPersonaIds: ReadonlySet<string> = new Set(),
5573
): PersonaLibraryState {
56-
const libraryPersonas = getActivePersonas(personas);
57-
const { catalogPersonas } = getCatalogSelectionState(personas);
74+
const libraryPersonas = getLibraryPersonas(personas);
75+
const { catalogPersonas } = getCatalogSelectionState(
76+
personas,
77+
sharedCatalogPersonaIds,
78+
);
5879

5980
return {
6081
catalogPersonas,

desktop/src/features/agents/ui/AgentsView.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { PersonaCatalogDialog } from "./PersonaCatalogDialog";
1111
import { PersonaDialog } from "./PersonaDialog";
1212
import { PersonaDeleteDialog } from "./PersonaDeleteDialog";
1313
import { PersonaImportUpdateDialog } from "./PersonaImportUpdateDialog";
14+
import { PersonaShareDialog } from "./PersonaShareDialog";
1415
import { RelayDirectorySection } from "./RelayDirectorySection";
1516
import { SecretRevealDialog } from "./SecretRevealDialog";
1617
import { TeamDeleteDialog } from "./TeamDeleteDialog";
@@ -137,8 +138,7 @@ export function AgentsView() {
137138
onCreatePersona={personas.openCreate}
138139
onChooseCatalog={personas.openCatalog}
139140
onDuplicatePersona={personas.openDuplicate}
140-
onEditPersona={personas.openEdit}
141-
onExportPersona={personas.handleExport}
141+
onSharePersona={personas.openShare}
142142
onDeactivatePersona={(persona) => {
143143
void personas.handleSetActive(persona, false, "library");
144144
}}
@@ -275,6 +275,35 @@ export function AgentsView() {
275275
persona={personas.personaToDelete}
276276
/>
277277
) : null}
278+
{personas.personaToShare ? (
279+
<PersonaShareDialog
280+
isCatalogVisible={
281+
personas.personaToShare.isBuiltIn ||
282+
personas.sharedCatalogPersonaIdSet.has(personas.personaToShare.id)
283+
}
284+
isPending={personas.isPending}
285+
onCatalogVisibilityChange={(visible) => {
286+
if (personas.personaToShare) {
287+
personas.setPersonaCatalogVisibility(
288+
personas.personaToShare,
289+
visible,
290+
);
291+
}
292+
}}
293+
onExport={() => {
294+
if (personas.personaToShare) {
295+
personas.handleExport(personas.personaToShare);
296+
}
297+
}}
298+
onOpenChange={(open) => {
299+
if (!open) {
300+
personas.setPersonaToShare(null);
301+
}
302+
}}
303+
open={personas.personaToShare !== null}
304+
persona={personas.personaToShare}
305+
/>
306+
) : null}
278307
{personas.isCatalogDialogOpen ? (
279308
<PersonaCatalogDialog
280309
error={

desktop/src/features/agents/ui/PersonaActionsMenu.tsx

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CopyPlus, Download, Ellipsis, Pencil, Trash2 } from "lucide-react";
1+
import { CopyPlus, Ellipsis, Share2, Trash2 } from "lucide-react";
22

33
import type { AgentPersona } from "@/shared/api/types";
44
import {
@@ -14,17 +14,15 @@ export function PersonaActionsMenu({
1414
isPending,
1515
persona,
1616
onDuplicate,
17-
onEdit,
18-
onExport,
17+
onShare,
1918
onDeactivate,
2019
onDelete,
2120
}: {
2221
isActionPending: boolean;
2322
isPending: boolean;
2423
persona: AgentPersona;
2524
onDuplicate: (persona: AgentPersona) => void;
26-
onEdit: (persona: AgentPersona) => void;
27-
onExport: (persona: AgentPersona) => void;
25+
onShare: (persona: AgentPersona) => void;
2826
onDeactivate: (persona: AgentPersona) => void;
2927
onDelete: (persona: AgentPersona) => void;
3028
}) {
@@ -45,34 +43,19 @@ export function PersonaActionsMenu({
4543
align="end"
4644
onCloseAutoFocus={(event) => event.preventDefault()}
4745
>
48-
{!persona.isBuiltIn ? (
49-
<DropdownMenuItem disabled={disabled} onClick={() => onEdit(persona)}>
50-
<Pencil className="h-4 w-4" />
51-
Edit
52-
</DropdownMenuItem>
53-
) : null}
46+
<DropdownMenuItem disabled={disabled} onClick={() => onShare(persona)}>
47+
<Share2 className="h-4 w-4" />
48+
Share
49+
</DropdownMenuItem>
5450
<DropdownMenuItem
5551
disabled={disabled}
5652
onClick={() => onDuplicate(persona)}
5753
>
5854
<CopyPlus className="h-4 w-4" />
5955
Duplicate
6056
</DropdownMenuItem>
61-
<DropdownMenuItem disabled={disabled} onClick={() => onExport(persona)}>
62-
<Download className="h-4 w-4" />
63-
Export
64-
</DropdownMenuItem>
6557
<DropdownMenuSeparator />
66-
{persona.isBuiltIn ? (
67-
<DropdownMenuItem
68-
className="text-destructive focus:text-destructive"
69-
disabled={disabled}
70-
onClick={() => onDeactivate(persona)}
71-
>
72-
<Trash2 className="h-4 w-4" />
73-
Remove from My Agents
74-
</DropdownMenuItem>
75-
) : persona.sourceTeam ? (
58+
{persona.sourceTeam ? (
7659
<DropdownMenuItem disabled>
7760
<Trash2 className="h-4 w-4" />
7861
Managed by team
@@ -81,10 +64,17 @@ export function PersonaActionsMenu({
8164
<DropdownMenuItem
8265
className="text-destructive focus:text-destructive"
8366
disabled={disabled}
84-
onClick={() => onDelete(persona)}
67+
onClick={() => {
68+
if (persona.isBuiltIn) {
69+
onDeactivate(persona);
70+
return;
71+
}
72+
73+
onDelete(persona);
74+
}}
8575
>
8676
<Trash2 className="h-4 w-4" />
87-
Delete
77+
Remove from My Agents
8878
</DropdownMenuItem>
8979
)}
9080
</DropdownMenuContent>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { cn } from "@/shared/lib/cn";
2+
3+
type PersonaAddedByProps = {
4+
className?: string;
5+
};
6+
7+
export function PersonaAddedBy({ className }: PersonaAddedByProps) {
8+
return (
9+
<p className={cn("truncate text-xs leading-tight", className)}>
10+
<span className="text-muted-foreground/55">Added by</span>
11+
{" "}
12+
<span className="text-muted-foreground">You</span>
13+
</p>
14+
);
15+
}

0 commit comments

Comments
 (0)