Files
buzz/desktop/src/features/agents/ui/personaLibraryCopy.ts
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w c9debf3620 feat(agents): re-home save-as-persona-template onto an existing agent
Adds a 'Save as persona template' action to an existing agent's menus,
re-homing the #1301 logic onto main's surfaces:

- saveAsPersonaTemplateDialogState() prefills the persona editor from a
  ManagedAgent, reverse-mapping the agent's harness command to a runtime
  id via commandsMatch (graceful fallback to no runtime). provider is
  omitted intentionally: ManagedAgent has no top-level provider field on
  main, so there is nothing lossless to carry.
- useSaveAsPersonaTemplate() is a self-contained hook (dialog + mutation)
  so a later visual reskin can move the trigger without untangling the
  behavior. Derives dialog state from agent + live runtimes via a memo to
  avoid the lazy-runtime-query first-open race.
- Wired into ManagedAgentRow's actions menu (gated personaId === null) and
  the sidebar agent profile (gated canEditAgent + persona-less).
- personaLibraryCopy.ts documents the UI<->backend vocabulary boundary:
  'persona template' is the product term for a backend persona (kind:30175).

Tests cover the dialog-state mapping: vocabulary, command reverse-map hit,
miss/empty-catalog fallback, null-command catalog entries, field carry-over,
provider omission, and null systemPrompt/model tolerance.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-06-26 13:55:01 -07:00

85 lines
3.6 KiB
TypeScript

/**
* UI copy for the persona library surfaces.
*
* ── Vocabulary boundary (UI ↔ backend) ──────────────────────────────────────
* The product-facing term is **"persona template"**: a reusable setup a user
* can save once and reuse to create more agents. The backend has no separate
* "template" concept — a persona template IS a **`persona`** (Nostr
* **kind:30175**). There is intentionally zero drift between the two:
*
* UI string backend entity / mutation
* ─────────────────────── ────────────────────────────────────────────────
* "persona template" ⇄ `persona` (kind:30175)
* "Save as persona ⇄ `createPersonaMutation` / `CreatePersonaInput`
* template"
*
* So every "...persona template" label here, in `CreateAgentDialog`'s opt-in
* toggle, and in the `useSaveAsPersonaTemplate` / `saveAsPersonaTemplateDialogState`
* save-as flow maps to the same backend `persona`. Keep new persona-template
* copy in this file and keep the mapping above current if the vocabulary moves.
*/
export const personaLibraryCopy = {
title: "My agents",
description:
"The personas you have chosen for this app. Use them to create teams and launch agents.",
chooseFromCatalog: "Choose...",
createNew: "Persona",
import: "Import",
emptyTitle: "No agents yet",
emptyDescription:
"Choose one from Persona Catalog, add your own persona, or import one to get started.",
emptyImportHint:
"Or drop a .persona.md, .persona.json, .persona.png, or .zip file here to import.",
} as const;
export const personaCatalogCopy = {
title: "Persona Catalog",
description: "Choose which built-in personas belong in My Agents.",
dialogTitle: "Choose from Persona Catalog",
dialogDescription:
"Select the built-in personas you want available in My Agents.",
emptyTitle: "You're all set",
emptyDescription: "Everything in Persona Catalog is already in My Agents.",
emptyCatalogDescription:
"New personas will show up here when the app ships more options.",
emptyCatalogTitle: "No personas in the catalog yet",
detailsAction: "View details",
selectAction: "Choose",
deselectAction: "Selected",
selectedState: "Selected",
availableState: "Available",
detailSelectedTitle: "Selected for My Agents",
detailSelectedDescription:
"Turn this off to remove the persona from teams and agent creation in this app.",
detailAvailableTitle: "Available in Persona Catalog",
detailAvailableDescription:
"Turn this on to make the persona available for teams and agent creation.",
teamEmptyState:
"No personas in My Agents yet. Create one or choose one from Persona Catalog first.",
} as const;
export function getPersonaCatalogSelectionActionCopy(isActive: boolean) {
return isActive
? personaCatalogCopy.deselectAction
: personaCatalogCopy.selectAction;
}
export function getPersonaCatalogSelectionAriaLabel(
displayName: string,
isActive: boolean,
) {
return `${isActive ? "Deselect" : "Select"} ${displayName} in My Agents`;
}
export function getPersonaCatalogDetailSelectionCopy(isActive: boolean) {
return isActive
? {
title: personaCatalogCopy.detailSelectedTitle,
description: personaCatalogCopy.detailSelectedDescription,
}
: {
title: personaCatalogCopy.detailAvailableTitle,
description: personaCatalogCopy.detailAvailableDescription,
};
}