diff --git a/src/renderer/src/screens/Chat/AgentAvatarContext.tsx b/src/renderer/src/screens/Chat/AgentAvatarContext.tsx new file mode 100644 index 000000000..52b7d7923 --- /dev/null +++ b/src/renderer/src/screens/Chat/AgentAvatarContext.tsx @@ -0,0 +1,20 @@ +import { createContext, useContext } from "react"; + +/** Appearance of the profile/agent that owns the current chat, so the avatar + * shown next to agent turns matches the one on the Profiles page. */ +export interface AgentAppearance { + /** Profile name — drives the letter/default colour fallback. */ + name?: string; + /** Accent colour for the letter fallback. */ + color?: string | null; + /** Custom avatar image as a data URL; when set it wins over the logo. */ + avatar?: string | null; +} + +const AgentAvatarContext = createContext({}); + +export const AgentAvatarProvider = AgentAvatarContext.Provider; + +export function useAgentAppearance(): AgentAppearance { + return useContext(AgentAvatarContext); +} diff --git a/src/renderer/src/screens/Chat/Chat.tsx b/src/renderer/src/screens/Chat/Chat.tsx index e5a842f86..6972c323f 100644 --- a/src/renderer/src/screens/Chat/Chat.tsx +++ b/src/renderer/src/screens/Chat/Chat.tsx @@ -4,6 +4,7 @@ import { Zap, Globe } from "lucide-react"; import { ChatInput, type ChatInputHandle } from "./ChatInput"; import { ChatEmptyState } from "./ChatEmptyState"; import { MessageList } from "./MessageList"; +import { AgentAvatarProvider } from "./AgentAvatarContext"; import { ModelPicker } from "./ModelPicker"; import { ReasoningEffortPicker } from "./ReasoningEffortPicker"; import { ContextFolderChip } from "./ContextFolderChip"; @@ -53,6 +54,9 @@ interface ChatProps { /** Whether this run is the one currently shown (drives keyboard handlers). */ active?: boolean; profile?: string; + /** Appearance (colour + custom avatar) of `profile`, so agent turns render + * the profile's avatar instead of the default Hermes logo (issue #679). */ + appearance?: { color?: string | null; avatar?: string | null }; onSessionStarted?: () => void; onNewChat?: () => void; /** Optional callback to navigate to Settings → Diagnose section @@ -74,6 +78,7 @@ function Chat({ initialSessionId, active = true, profile, + appearance, onSessionStarted, onNewChat, onOpenDiagnose, @@ -846,18 +851,26 @@ function Chat({
- {messages.length === 0 ? ( - - ) : ( - - )} + + {messages.length === 0 ? ( + + ) : ( + + )} +
diff --git a/src/renderer/src/screens/Chat/ChatEmptyState.tsx b/src/renderer/src/screens/Chat/ChatEmptyState.tsx index 3b6cc672a..868cc0769 100644 --- a/src/renderer/src/screens/Chat/ChatEmptyState.tsx +++ b/src/renderer/src/screens/Chat/ChatEmptyState.tsx @@ -2,6 +2,8 @@ import { memo } from "react"; import { Search, Clock, Mail, Code, ChartLine, Bell } from "lucide-react"; import titleLine from "../../assets/title-line.svg"; import { useI18n } from "../../components/useI18n"; +import ProfileAvatar from "../../components/common/ProfileAvatar"; +import { useAgentAppearance } from "./AgentAvatarContext"; interface Suggestion { i18nKey: string; @@ -50,19 +52,33 @@ export const ChatEmptyState = memo(function ChatEmptyState({ onSelectSuggestion, }: ChatEmptyStateProps): React.JSX.Element { const { t } = useI18n(); + // Show the current agent's avatar (custom image or coloured initial) so the + // empty state matches who you're about to talk to. The default profile keeps + // the Hermes wordmark logo (issue #679). + const { avatar, name, color } = useAgentAppearance(); + const useProfileAvatar = !!avatar || (!!name && name !== "default"); return (
- + {useProfileAvatar ? ( + + ) : ( + + )}
{t("chat.emptyTitle")}
{t("chat.emptyHint")}
diff --git a/src/renderer/src/screens/Chat/MessageRow.tsx b/src/renderer/src/screens/Chat/MessageRow.tsx index 3c906a58e..eef290f29 100644 --- a/src/renderer/src/screens/Chat/MessageRow.tsx +++ b/src/renderer/src/screens/Chat/MessageRow.tsx @@ -5,7 +5,9 @@ import { AgentMarkdown } from "../../components/AgentMarkdown"; import { AttachmentChip } from "../../components/AttachmentChip"; import { MediaSegmentView } from "../../components/MediaImage"; import { useI18n } from "../../components/useI18n"; +import ProfileAvatar from "../../components/common/ProfileAvatar"; import { parseMediaTokens, cleanLeakedToolTags } from "./mediaUtils"; +import { useAgentAppearance } from "./AgentAvatarContext"; import type { ChatBubbleMessage, ChatMessage } from "./types"; export const APPROVAL_RE = @@ -72,6 +74,13 @@ export const HermesAvatar = memo(function HermesAvatar({ /** True only for the avatar of the turn currently being generated. */ active?: boolean; }): React.JSX.Element { + // A profile with its own identity (a custom avatar image, or any non-default + // named profile with its coloured initial) shows that avatar — matching the + // Profiles page (issue #679). The default profile keeps the animated Hermes + // mark below. Hooks still run unconditionally; only the render branches. + const { avatar, name, color } = useAgentAppearance(); + const useProfileAvatar = !!avatar || (!!name && name !== "default"); + const [frozenSrc, setFrozenSrc] = useState(null); const [playing, setPlaying] = useState(active); // Re-keying the on each play session restarts the gif from frame 0 so @@ -124,7 +133,14 @@ export const HermesAvatar = memo(function HermesAvatar({ return (
- {playing ? ( + {useProfileAvatar ? ( + + ) : playing ? ( ) : ( diff --git a/src/renderer/src/screens/Layout/Layout.tsx b/src/renderer/src/screens/Layout/Layout.tsx index b62eb8331..3e8cd8781 100644 --- a/src/renderer/src/screens/Layout/Layout.tsx +++ b/src/renderer/src/screens/Layout/Layout.tsx @@ -443,6 +443,24 @@ function Layout({ return () => window.removeEventListener("keydown", onKeyDown); }, [sessionsModalOpen]); + // Pin the visible, not-yet-started chat to the active profile. This is the + // safety net behind every path that changes the active profile — launch-time + // restore of the persisted active_profile, the ProfileSwitcher, deep links — + // so a fresh chat always routes to the selected agent's gateway and memory. + // Without it a chat minted under "default" keeps talking to default (and its + // memory) even after the active profile has switched (issue #679 follow-up). + useEffect(() => { + setRuns((prev) => + prev.map((r) => + r.runId === activeRunId && + isScratchRun(r) && + r.profile !== activeProfile + ? { ...r, profile: activeProfile } + : r, + ), + ); + }, [activeProfile, activeRunId]); + const handleSelectProfile = useCallback( (name: string) => { // Selecting an agent is administrative: switch the active profile (the @@ -757,6 +775,7 @@ function Layout({ initialSessionId={run.sessionId} active={run.runId === activeRunId} profile={run.profile} + appearance={getAppearance(run.profile)} onNewChat={handleNewChat} onOpenDiagnose={() => goTo("settings")} onLoadingChange={handleRunLoading}