mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: make personas optional (#51)
* feat: make personas optional — allow chatting without a persona Adds a "Goose" / no-persona option to the persona picker so users can chat directly with the agent without any persona system prompt injected. HomeScreen and ChatView now default to no persona selected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update HomeScreen test for optional persona default The HomeScreen now defaults to no persona selected, so the placeholder reads "Message Goose..." instead of "Message Solo...". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ interface ChatInputProps {
|
||||
// Personas
|
||||
personas?: Persona[];
|
||||
selectedPersonaId?: string | null;
|
||||
onPersonaChange?: (personaId: string) => void;
|
||||
onPersonaChange?: (personaId: string | null) => void;
|
||||
onCreatePersona?: () => void;
|
||||
// Provider (secondary -- auto-set by persona but overridable)
|
||||
providers?: AcpProvider[];
|
||||
@@ -97,20 +97,11 @@ export function ChatInput({
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const defaultPersonaId = useMemo(
|
||||
() => personas.find((persona) => persona.id === "builtin-solo")?.id ?? null,
|
||||
[personas],
|
||||
);
|
||||
const activePersona = useMemo(
|
||||
() => personas.find((persona) => persona.id === selectedPersonaId) ?? null,
|
||||
[personas, selectedPersonaId],
|
||||
);
|
||||
const stickyPersona =
|
||||
activePersona &&
|
||||
selectedPersonaId !== null &&
|
||||
selectedPersonaId !== defaultPersonaId
|
||||
? activePersona
|
||||
: null;
|
||||
const stickyPersona = activePersona;
|
||||
|
||||
const canSend = text.trim().length > 0 && !isStreaming && !disabled;
|
||||
|
||||
@@ -213,10 +204,8 @@ export function ChatInput({
|
||||
: placeholder;
|
||||
|
||||
const handleClearStickyPersona = useCallback(() => {
|
||||
if (defaultPersonaId) {
|
||||
onPersonaChange?.(defaultPersonaId);
|
||||
}
|
||||
}, [defaultPersonaId, onPersonaChange]);
|
||||
onPersonaChange?.(null);
|
||||
}, [onPersonaChange]);
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
|
||||
@@ -52,7 +52,7 @@ interface ChatInputToolbarProps {
|
||||
// Personas
|
||||
personas: Persona[];
|
||||
selectedPersonaId: string | null;
|
||||
onPersonaChange?: (personaId: string) => void;
|
||||
onPersonaChange?: (personaId: string | null) => void;
|
||||
onCreatePersona?: () => void;
|
||||
// Provider
|
||||
providers: AcpProvider[];
|
||||
|
||||
@@ -49,8 +49,8 @@ export function ChatView({
|
||||
|
||||
// Persona state
|
||||
const personas = useAgentStore((s) => s.personas);
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState<string>(
|
||||
initialPersonaId ?? "builtin-solo",
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState<string | null>(
|
||||
initialPersonaId ?? null,
|
||||
);
|
||||
const session = useChatSessionStore((s) =>
|
||||
s.sessions.find((candidate) => candidate.id === activeSessionId),
|
||||
@@ -171,7 +171,7 @@ export function ChatView({
|
||||
|
||||
// When persona changes, update the provider to match persona's default
|
||||
const handlePersonaChange = useCallback(
|
||||
(personaId: string) => {
|
||||
(personaId: string | null) => {
|
||||
setSelectedPersonaId(personaId);
|
||||
const persona = personas.find((p) => p.id === personaId);
|
||||
if (persona?.provider) {
|
||||
@@ -197,7 +197,7 @@ export function ChatView({
|
||||
// Persist persona selection to session store
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(activeSessionId, { personaId });
|
||||
.updateSession(activeSessionId, { personaId: personaId ?? undefined });
|
||||
},
|
||||
[personas, providers, activeSessionId, handleProviderChange],
|
||||
);
|
||||
@@ -205,19 +205,14 @@ export function ChatView({
|
||||
// Validate persona still exists — fall back to default if deleted
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedPersonaId !== null &&
|
||||
personas.length > 0 &&
|
||||
!personas.find((p) => p.id === selectedPersonaId)
|
||||
) {
|
||||
const fallback =
|
||||
personas.find((p) => p.id === "builtin-solo") ?? personas[0];
|
||||
if (fallback) {
|
||||
setSelectedPersonaId(fallback.id);
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(activeSessionId, { personaId: fallback.id });
|
||||
}
|
||||
// Selected persona was deleted — reset to no persona
|
||||
setSelectedPersonaId(null);
|
||||
}
|
||||
}, [personas, selectedPersonaId, activeSessionId]);
|
||||
}, [personas, selectedPersonaId]);
|
||||
|
||||
const displayAgentName = selectedPersona?.displayName ?? agentName;
|
||||
const personaAvatarSrc = useAvatarSrc(selectedPersona?.avatar);
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { Persona } from "@/shared/types/agents";
|
||||
interface PersonaPickerProps {
|
||||
personas: Persona[];
|
||||
selectedPersonaId: string | null;
|
||||
onPersonaChange: (personaId: string) => void;
|
||||
onPersonaChange: (personaId: string | null) => void;
|
||||
onCreatePersona?: () => void;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
@@ -33,7 +33,10 @@ export function PersonaPicker({
|
||||
triggerVariant = "default",
|
||||
}: PersonaPickerProps) {
|
||||
const selected = useMemo(
|
||||
() => personas.find((p) => p.id === selectedPersonaId) ?? personas[0],
|
||||
() =>
|
||||
selectedPersonaId
|
||||
? personas.find((p) => p.id === selectedPersonaId)
|
||||
: undefined,
|
||||
[personas, selectedPersonaId],
|
||||
);
|
||||
|
||||
@@ -46,7 +49,7 @@ export function PersonaPicker({
|
||||
[personas],
|
||||
);
|
||||
|
||||
const label = selected?.displayName ?? "Select persona";
|
||||
const label = selected?.displayName ?? "Goose";
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -85,6 +88,24 @@ export function PersonaPicker({
|
||||
align="start"
|
||||
className="max-h-[min(70vh,32rem)] w-[22rem] overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => onPersonaChange(null)}
|
||||
className="flex items-start gap-2.5 py-2"
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-foreground/10 text-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="text-sm font-medium">Goose</span>
|
||||
<span className="text-[11px] leading-snug text-foreground-tertiary">
|
||||
No persona — chat directly with the agent
|
||||
</span>
|
||||
</div>
|
||||
{selectedPersonaId === null && (
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-foreground-secondary" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{builtinPersonas.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-[11px] uppercase tracking-wider text-foreground-tertiary">
|
||||
|
||||
@@ -29,7 +29,9 @@ function StatefulChatInput({
|
||||
}: {
|
||||
onSend?: (text: string, personaId?: string) => void;
|
||||
}) {
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState("builtin-solo");
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState<string | null>(
|
||||
"builtin-solo",
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatInput
|
||||
|
||||
@@ -57,10 +57,10 @@ describe("HomeScreen", () => {
|
||||
expect(screen.getByText("Good afternoon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the chat input placeholder with persona name", () => {
|
||||
it("renders the chat input placeholder with default agent name when no persona selected", () => {
|
||||
render(<HomeScreen />);
|
||||
expect(
|
||||
screen.getByPlaceholderText("Message Solo... (type @ to mention)"),
|
||||
screen.getByPlaceholderText("Message Goose... (type @ to mention)"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ export function HomeScreen({
|
||||
|
||||
const personas = useAgentStore((s) => s.personas);
|
||||
const projects = useProjectStore((s) => s.projects);
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState("builtin-solo");
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [providers, setProviders] = useState<AcpProvider[]>([]);
|
||||
const [selectedProvider, setSelectedProvider] = useState("goose");
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
||||
@@ -96,7 +98,7 @@ export function HomeScreen({
|
||||
|
||||
const handleSend = useCallback(
|
||||
(message: string, personaId?: string) => {
|
||||
const effectivePersonaId = personaId ?? selectedPersonaId;
|
||||
const effectivePersonaId = personaId ?? selectedPersonaId ?? undefined;
|
||||
|
||||
onStartChat?.(
|
||||
message,
|
||||
|
||||
Reference in New Issue
Block a user