fix(frontend): persist selected agent mode across page refresh (planning agent) (#12672)

This commit is contained in:
Hiep Le
2026-01-30 15:39:36 +07:00
committed by GitHub
parent 59834beba7
commit c8b867a634
5 changed files with 175 additions and 6 deletions

View File

@@ -1,7 +1,8 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";
import {
clearConversationLocalStorage,
getConversationState,
isTaskConversationId,
setConversationState,
LOCAL_STORAGE_KEYS,
} from "#/utils/conversation-local-storage";
@@ -11,6 +12,76 @@ describe("conversation localStorage utilities", () => {
localStorage.clear();
});
describe("isTaskConversationId", () => {
it("returns true for IDs starting with task-", () => {
expect(isTaskConversationId("task-abc-123")).toBe(true);
expect(isTaskConversationId("task-")).toBe(true);
});
it("returns false for normal conversation IDs", () => {
expect(isTaskConversationId("conv-123")).toBe(false);
expect(isTaskConversationId("abc")).toBe(false);
});
});
describe("getConversationState", () => {
it("returns default state including conversationMode for task IDs without reading localStorage", () => {
const state = getConversationState("task-uuid-123");
expect(state.conversationMode).toBe("code");
expect(state.selectedTab).toBe("editor");
expect(state.rightPanelShown).toBe(true);
expect(
localStorage.getItem(
`${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-task-uuid-123`,
),
).toBeNull();
});
it("returns merged state from localStorage for real conversation ID including conversationMode", () => {
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-conv-1`;
localStorage.setItem(
key,
JSON.stringify({ conversationMode: "plan", selectedTab: "terminal" }),
);
const state = getConversationState("conv-1");
expect(state.conversationMode).toBe("plan");
expect(state.selectedTab).toBe("terminal");
expect(state.rightPanelShown).toBe(true);
});
it("returns default state when key is missing or invalid", () => {
expect(getConversationState("conv-missing").conversationMode).toBe(
"code",
);
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-conv-bad`;
localStorage.setItem(key, "not json");
expect(getConversationState("conv-bad").conversationMode).toBe("code");
});
});
describe("setConversationState", () => {
it("does not persist when conversationId is a task ID", () => {
setConversationState("task-xyz", { conversationMode: "plan" });
expect(
localStorage.getItem(
`${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-task-xyz`,
),
).toBeNull();
});
it("persists conversationMode for real conversation ID and getConversationState returns it", () => {
setConversationState("conv-2", { conversationMode: "plan" });
const state = getConversationState("conv-2");
expect(state.conversationMode).toBe("plan");
});
});
describe("clearConversationLocalStorage", () => {
it("removes the consolidated conversation-state localStorage entry", () => {
const conversationId = "conv-123";

View File

@@ -87,6 +87,7 @@ describe("useHandlePlanClick", () => {
rightPanelShown: true,
unpinnedTabs: [],
subConversationTaskId: null,
conversationMode: "code",
});
});
@@ -115,6 +116,7 @@ describe("useHandlePlanClick", () => {
rightPanelShown: true,
unpinnedTabs: [],
subConversationTaskId: storedTaskId,
conversationMode: "code",
});
renderHook(() => useHandlePlanClick());
@@ -152,6 +154,7 @@ describe("useHandlePlanClick", () => {
rightPanelShown: true,
unpinnedTabs: [],
subConversationTaskId: storedTaskId,
conversationMode: "code",
});
renderHook(() => useHandlePlanClick());

View File

@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useConversationStore } from "#/stores/conversation-store";
const defaultConversationState = {
selectedTab: "editor" as const,
rightPanelShown: true,
unpinnedTabs: [] as string[],
conversationMode: "code" as const,
};
const mockGetConversationState = vi.fn((_id: string) => defaultConversationState);
const mockSetConversationState = vi.fn();
vi.mock("#/utils/conversation-local-storage", () => ({
getConversationState: (id: string) => mockGetConversationState(id),
setConversationState: (id: string, updates: object) =>
mockSetConversationState(id, updates),
}));
const CONV_ID = "conv-test-1";
describe("conversation store", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetConversationState.mockReturnValue(defaultConversationState);
Object.defineProperty(window, "location", {
value: { pathname: `/conversations/${CONV_ID}` },
writable: true,
});
useConversationStore.setState({
conversationMode: "code",
planContent: null,
subConversationTaskId: null,
shouldHideSuggestions: false,
});
});
describe("setConversationMode", () => {
it("updates store state and persists via setConversationState when conversation ID is in location", () => {
useConversationStore.getState().setConversationMode("plan");
expect(useConversationStore.getState().conversationMode).toBe("plan");
expect(mockSetConversationState).toHaveBeenCalledWith(CONV_ID, {
conversationMode: "plan",
});
});
});
describe("resetConversationState", () => {
it("sets conversationMode from getConversationState", () => {
useConversationStore.setState({ conversationMode: "plan" });
mockGetConversationState.mockReturnValue({
selectedTab: "editor",
rightPanelShown: true,
unpinnedTabs: [],
conversationMode: "code",
});
useConversationStore.getState().resetConversationState();
expect(useConversationStore.getState().conversationMode).toBe("code");
expect(mockGetConversationState).toHaveBeenCalledWith(CONV_ID);
});
});
});

View File

@@ -1,5 +1,9 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import {
getConversationState,
setConversationState,
} from "#/utils/conversation-local-storage";
export type ConversationTab =
| "editor"
@@ -105,6 +109,20 @@ const getInitialRightPanelState = (): boolean => {
return true;
};
const getInitialConversationMode = (): ConversationMode => {
if (typeof window === "undefined") {
return "code";
}
const conversationId = getConversationIdFromLocation();
if (!conversationId) {
return "code";
}
const state = getConversationState(conversationId);
return state.conversationMode;
};
export const useConversationStore = create<ConversationStore>()(
devtools(
(set) => ({
@@ -121,7 +139,7 @@ export const useConversationStore = create<ConversationStore>()(
shouldHideSuggestions: false,
hasRightPanelToggled: true,
planContent: null,
conversationMode: "code",
conversationMode: getInitialConversationMode(),
subConversationTaskId: null,
// Actions
@@ -257,7 +275,7 @@ export const useConversationStore = create<ConversationStore>()(
set(
{
shouldHideSuggestions: false,
conversationMode: "code",
conversationMode: getInitialConversationMode(),
subConversationTaskId: null,
planContent: null,
},
@@ -268,8 +286,13 @@ export const useConversationStore = create<ConversationStore>()(
setHasRightPanelToggled: (hasRightPanelToggled) =>
set({ hasRightPanelToggled }, false, "setHasRightPanelToggled"),
setConversationMode: (conversationMode) =>
set({ conversationMode }, false, "setConversationMode"),
setConversationMode: (conversationMode) => {
const conversationId = getConversationIdFromLocation();
if (conversationId) {
setConversationState(conversationId, { conversationMode });
}
set({ conversationMode }, false, "setConversationMode");
},
setSubConversationTaskId: (subConversationTaskId) =>
set({ subConversationTaskId }, false, "setSubConversationTaskId"),

View File

@@ -1,5 +1,8 @@
import { useState } from "react";
import type { ConversationTab } from "#/stores/conversation-store";
import type {
ConversationTab,
ConversationMode,
} from "#/stores/conversation-store";
export const LOCAL_STORAGE_KEYS = {
CONVERSATION_STATE: "conversation-state",
@@ -12,6 +15,7 @@ export interface ConversationState {
selectedTab: ConversationTab | null;
rightPanelShown: boolean;
unpinnedTabs: string[];
conversationMode: ConversationMode;
subConversationTaskId: string | null;
}
@@ -19,6 +23,7 @@ const DEFAULT_CONVERSATION_STATE: ConversationState = {
selectedTab: "editor",
rightPanelShown: true,
unpinnedTabs: [],
conversationMode: "code",
subConversationTaskId: null,
};
@@ -93,6 +98,7 @@ export function useConversationLocalStorageState(conversationId: string): {
setSelectedTab: (tab: ConversationTab | null) => void;
setRightPanelShown: (shown: boolean) => void;
setUnpinnedTabs: (tabs: string[]) => void;
setConversationMode: (mode: ConversationMode) => void;
} {
const [state, setState] = useState<ConversationState>(() =>
getConversationState(conversationId),
@@ -108,5 +114,6 @@ export function useConversationLocalStorageState(conversationId: string): {
setSelectedTab: (tab) => updateState({ selectedTab: tab }),
setRightPanelShown: (shown) => updateState({ rightPanelShown: shown }),
setUnpinnedTabs: (tabs) => updateState({ unpinnedTabs: tabs }),
setConversationMode: (mode) => updateState({ conversationMode: mode }),
};
}