diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 21f7c5f7..7a92a236 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -709,6 +709,31 @@ "No tools available": "No tools available", "Created successfully": "Created successfully", "Deleted successfully": "Deleted successfully", + "Agent roles": "Agent roles", + "Personas": "Personas", + "Add role": "Add role", + "Edit role": "Edit role", + "Delete role": "Delete role", + "Role name": "Role name", + "Emoji": "Emoji", + "Optional. Shown next to the role name in the picker.": "Optional. Shown next to the role name in the picker.", + "Description": "Description", + "Optional. Shown in the role picker to help users choose.": "Optional. Shown in the role picker to help users choose.", + "Instructions": "Instructions", + "The agent persona. Replaces the workspace system message for chats bound to this role. A built-in safety framework is always appended.": "The agent persona. Replaces the workspace system message for chats bound to this role. A built-in safety framework is always appended.", + "Model override": "Model override", + "Optional. Use a different model for chats bound to this role.": "Optional. Use a different model for chats bound to this role.", + "Use workspace default": "Use workspace default", + "Custom model": "Custom model", + "Driver": "Driver", + "Defaults to the workspace driver. Credentials for an alternate driver must be set in the provider settings.": "Defaults to the workspace driver. Credentials for an alternate driver must be set in the provider settings.", + "No agent roles configured": "No agent roles configured", + "Reusable agent personas for specialized chats.": "Reusable agent personas for specialized chats.", + "Are you sure you want to delete this role? Existing chats keep their persona.": "Are you sure you want to delete this role? Existing chats keep their persona.", + "Disabled roles are hidden from the picker but existing chats keep using them.": "Disabled roles are hidden from the picker but existing chats keep using them.", + "Universal assistant": "Universal assistant", + "Agent role": "Agent role", + "Default workspace persona": "Default workspace persona", "Clear": "Clear", "Provider": "Provider", "•••• set": "•••• set", diff --git a/apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts b/apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts index b3707cb9..9b5c0728 100644 --- a/apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts +++ b/apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts @@ -19,3 +19,12 @@ export const aiChatWindowOpenAtom = atom(false); // in ChatInput's local state, that remount would wipe text the user typed while // the agent was still streaming. Reset on deliberate chat switches. export const aiChatDraftAtom = atom(""); + +/** + * The role selected for the NEXT new chat (picker). Bound to the chat only on + * the first turn (sent as `roleId` in the `/ai-chat/stream` body when chatId is + * null); after that the role is fixed on the chat row and this atom is not read + * again for that chat. Reset to null (universal assistant) on "New chat". null + * means the universal assistant (no bound role). + */ +export const selectedRoleForNewChatAtom = atom(null as string | null); diff --git a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx index 122f80ff..f686d5b7 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx +++ b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx @@ -6,7 +6,12 @@ import { useRef, useState, } from "react"; -import { Group, Loader, Tooltip } from "@mantine/core"; +import { + Group, + Loader, + Select, + Tooltip, +} from "@mantine/core"; import { IconArrowsDiagonal, IconCheck, @@ -25,6 +30,7 @@ import { activeAiChatIdAtom, aiChatWindowOpenAtom, aiChatDraftAtom, + selectedRoleForNewChatAtom, } from "@/features/ai-chat/atoms/ai-chat-atom.ts"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { extractPageSlugId } from "@/lib"; @@ -33,6 +39,7 @@ import { useAiChatMessagesQuery, useAiChatsQuery, } from "@/features/ai-chat/queries/ai-chat-query.ts"; +import { useAiRolesQuery } from "@/features/workspace/queries/ai-agent-roles-query.ts"; import ConversationList from "@/features/ai-chat/components/conversation-list.tsx"; import ChatThread from "@/features/ai-chat/components/chat-thread.tsx"; import { buildChatMarkdown } from "@/features/ai-chat/utils/chat-markdown.ts"; @@ -102,6 +109,13 @@ export default function AiChatWindow() { const [windowOpen, setWindowOpen] = useAtom(aiChatWindowOpenAtom); const [activeChatId, setActiveChatId] = useAtom(activeAiChatIdAtom); const setDraft = useSetAtom(aiChatDraftAtom); + const [selectedRoleId, setSelectedRoleId] = useAtom( + selectedRoleForNewChatAtom, + ); + + // Roles for the chat-start picker (any workspace member can list). Loaded + // unconditionally so the picker is ready when the user starts a new chat. + const { data: roles } = useAiRolesQuery(); // History section starts collapsed (matches the former panel's behavior). const [historyOpen, setHistoryOpen] = useState(false); @@ -144,7 +158,8 @@ export default function AiChatWindow() { setActiveChatId(null); setHistoryOpen(false); setDraft(""); - }, [setActiveChatId, setDraft]); + setSelectedRoleId(null); + }, [setActiveChatId, setDraft, setSelectedRoleId]); const selectChat = useCallback( (chatId: string): void => { @@ -342,6 +357,17 @@ export default function AiChatWindow() { style={{ flex: "none" }} /> {t("AI chat")} + {activeChat?.roleName && ( + + + {activeChat.roleEmoji ? `${activeChat.roleEmoji} ` : ""} + {activeChat.roleName} + + + )}
{contextTokens > 0 && ( @@ -432,6 +458,33 @@ export default function AiChatWindow() { )}
+ {/* Role picker for a NEW chat only. The role is bound once at chat + creation; for an existing chat the role is fixed on the chat row and + shown as a badge in the drag bar above. Hidden when no roles exist. */} + {activeChatId === null && roles && roles.length > 0 && ( +
+