Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions desktop/renderer/src/components/SidePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
11 changes: 10 additions & 1 deletion desktop/renderer/src/components/chat/ChatMessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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" } });
}
Expand Down
14 changes: 14 additions & 0 deletions desktop/renderer/src/stores/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──

Expand Down
21 changes: 19 additions & 2 deletions desktop/renderer/src/stores/chat.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -152,6 +152,18 @@ export const useChatStore = defineStore("chat", () => {
const pendingPrompt = ref<string | null>(null);
const pendingSessionAgentId = ref<string | undefined>(undefined);

/**
* The agent bound to the currently-active session.
* Non-default agents are encoded into the session key as `agent:<id>:...`;
* 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<Record<string, string>>({});

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1213,6 +1229,7 @@ export const useChatStore = defineStore("chat", () => {
return {
sessionKey,
resolvedSessionKey,
currentSessionAgentId,
messages,
loading,
sending,
Expand Down
Loading