diff --git a/desktop/renderer/src/components/SidePanel.vue b/desktop/renderer/src/components/SidePanel.vue index 39874c3..c5fb22b 100644 --- a/desktop/renderer/src/components/SidePanel.vue +++ b/desktop/renderer/src/components/SidePanel.vue @@ -219,16 +219,22 @@ onMounted(() => { gateway.refreshWeixinStatus(); }); -/** Only create a new session if the current one already has messages. */ +/** + * Ensure the active chat is a fresh session bound to the currently-selected agent. + * Starts a new session when the current chat already has messages, OR when it + * belongs to a different agent than the one now selected — otherwise switching + * agents on an empty chat would keep routing to (and displaying) the old agent. + */ function ensureEmptySession() { - if (chatStore.messages.length > 0) { - chatStore.newSession(agentStore.currentAgentId); + const targetAgentId = agentStore.currentAgentId; + if (chatStore.messages.length > 0 || chatStore.currentSessionAgentId !== targetAgentId) { + chatStore.newSession(targetAgentId); } - // Tag the (new or existing) session with the current agent + // Tag the (new or existing) session with the current agent. const key = chatStore.sessionKey; const s = sessionStore.sessions.find((s) => s.key === key); - if (s && !s.agentId) { - s.agentId = agentStore.currentAgentId; + if (s) { + s.agentId = targetAgentId; } } diff --git a/desktop/renderer/src/components/chat/ChatMessageList.vue b/desktop/renderer/src/components/chat/ChatMessageList.vue index 1c9d76b..e0d1041 100644 --- a/desktop/renderer/src/components/chat/ChatMessageList.vue +++ b/desktop/renderer/src/components/chat/ChatMessageList.vue @@ -467,6 +467,7 @@ import { ref, computed, nextTick, watch } from "vue"; import { useRouter } from "vue-router"; import { useChatStore } from "@/stores/chat"; +import { useAgentStore } from "@/stores/agents"; import { renderMarkdown, stripAnsi, @@ -485,15 +486,23 @@ import searchGif from "@/assets/openclaw_search_preview_transparent.gif"; import bookGif from "@/assets/book.gif"; import binocularsGif from "@/assets/binoculars.gif"; import mapGif from "@/assets/map.gif"; -import avatarImg from "@/assets/normal.png"; +import defaultAvatar from "@/assets/normal.png"; defineProps<{ studioMode?: boolean; }>(); const chatStore = useChatStore(); +const agentStore = useAgentStore(); const router = useRouter(); +/** Avatar of the agent bound to the current session (falls back to the default). */ +const avatarImg = computed(() => { + const agentId = chatStore.currentSessionAgentId; + const agent = agentStore.agents.find((a) => a.id === agentId); + return agent?.avatar || defaultAvatar; +}); + function openModelSettings() { router.push({ name: "settings", params: { section: "models" } }); } diff --git a/desktop/renderer/src/stores/chat.test.ts b/desktop/renderer/src/stores/chat.test.ts index d5976c5..8dffb40 100644 --- a/desktop/renderer/src/stores/chat.test.ts +++ b/desktop/renderer/src/stores/chat.test.ts @@ -437,6 +437,20 @@ describe("useChatStore — draft sessions", () => { ), ).toBe(true); }); + + it("encodes a non-default agent into the session key so the gateway routes to it", () => { + const store = useChatStore(); + store.newSession("coder"); + expect(store.sessionKey).toMatch(/^agent:coder:session-/); + expect(store.currentSessionAgentId).toBe("coder"); + }); + + it("keeps a bare session key for the default (main) agent", () => { + const store = useChatStore(); + store.newSession("main"); + expect(store.sessionKey).toMatch(/^session-/); + expect(store.currentSessionAgentId).toBe("main"); + }); }); // ── Message content extraction tests ── diff --git a/desktop/renderer/src/stores/chat.ts b/desktop/renderer/src/stores/chat.ts index 9ea365d..b8c4f1e 100644 --- a/desktop/renderer/src/stores/chat.ts +++ b/desktop/renderer/src/stores/chat.ts @@ -1,5 +1,5 @@ import { defineStore } from "pinia"; -import { ref } from "vue"; +import { ref, computed } from "vue"; import { useSessionStore } from "./sessions"; import { scanPii, redactPii } from "@/utils/pii-scanner"; @@ -152,6 +152,18 @@ export const useChatStore = defineStore("chat", () => { const pendingPrompt = ref(null); const pendingSessionAgentId = ref(undefined); + /** + * The agent bound to the currently-active session. + * Non-default agents are encoded into the session key as `agent::...`; + * for a freshly-created session not yet synced, fall back to the pending + * agent; otherwise the session belongs to the default (main) agent. + */ + const currentSessionAgentId = computed(() => { + const match = /^agent:([^:]+):/.exec(sessionKey.value); + if (match) return match[1]; + return pendingSessionAgentId.value || "main"; + }); + /** Per-agent last message preview text */ const lastMessageMap = ref>({}); @@ -1025,7 +1037,11 @@ export const useChatStore = defineStore("chat", () => { _syncToSessionStore(); _saveCurrentState(); - const key = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const suffix = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + // Encode the agent into the session key so the gateway routes the message + // to the selected agent. Bare keys (and "main") are normalised to the + // default (main) agent server-side, so only non-default agents are prefixed. + const key = agentId && agentId !== "main" ? `agent:${agentId}:${suffix}` : suffix; pendingSessionAgentId.value = agentId; sessionKey.value = key; resolvedSessionKey.value = null; @@ -1213,6 +1229,7 @@ export const useChatStore = defineStore("chat", () => { return { sessionKey, resolvedSessionKey, + currentSessionAgentId, messages, loading, sending,