Fix tab pin/unpin by aligning localStorage key per conversation- #12287 (#12292)

Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
This commit is contained in:
Jatoth Adithya Naik
2026-01-13 20:55:08 +05:30
committed by GitHub
parent d7218925c4
commit 9d405243b8
2 changed files with 99 additions and 42 deletions

View File

@@ -0,0 +1,82 @@
import { describe, it, expect, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router";
import { ConversationTabsContextMenu } from "#/components/features/conversation/conversation-tabs/conversation-tabs-context-menu";
function renderWithRouter(conversationId: string, onClose: () => void) {
return render(
<MemoryRouter initialEntries={[`/conversations/${conversationId}`]}>
<Routes>
<Route
path="/conversations/:conversationId"
element={<ConversationTabsContextMenu isOpen onClose={onClose} />}
/>
</Routes>
</MemoryRouter>,
);
}
describe("ConversationTabsContextMenu", () => {
afterEach(() => {
localStorage.clear();
});
it("should use per-conversation localStorage key for unpinned tabs", async () => {
const user = userEvent.setup();
const onClose = () => {};
// Render for conversation-1
const { unmount } = renderWithRouter("conversation-1", onClose);
// Unpin the terminal tab in conversation-1
const terminalItem = screen.getByText("COMMON$TERMINAL");
await user.click(terminalItem);
// Verify localStorage key is per-conversation
const stored1 = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored1).toContain("terminal");
unmount();
// Switch to conversation-2
renderWithRouter("conversation-2", onClose);
// conversation-2 should have its own empty state
const stored2 = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-2") || "[]",
);
expect(stored2).toEqual([]);
// conversation-1 state should still have terminal unpinned
const stored1Again = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored1Again).toContain("terminal");
});
it("should toggle tab pin state when clicked", async () => {
const user = userEvent.setup();
const onClose = () => {};
renderWithRouter("conversation-1", onClose);
const terminalItem = screen.getByText("COMMON$TERMINAL");
// Click to unpin
await user.click(terminalItem);
let stored = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored).toContain("terminal");
// Click again to pin
await user.click(terminalItem);
stored = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored).not.toContain("terminal");
});
});

View File

@@ -13,6 +13,7 @@ import PillIcon from "#/icons/pill.svg?react";
import PillFillIcon from "#/icons/pill-fill.svg?react";
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { useConversationId } from "#/hooks/use-conversation-id";
interface ConversationTabsContextMenuProps {
isOpen: boolean;
@@ -25,39 +26,21 @@ export function ConversationTabsContextMenu({
}: ConversationTabsContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
const { t } = useTranslation();
const { conversationId } = useConversationId();
const [unpinnedTabs, setUnpinnedTabs] = useLocalStorage<string[]>(
"conversation-unpinned-tabs",
`conversation-unpinned-tabs-${conversationId}`,
[],
);
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
const tabConfig = [
{
tab: "editor",
icon: GitChanges,
i18nKey: I18nKey.COMMON$CHANGES,
},
{
tab: "vscode",
icon: VSCodeIcon,
i18nKey: I18nKey.COMMON$CODE,
},
{
tab: "terminal",
icon: TerminalIcon,
i18nKey: I18nKey.COMMON$TERMINAL,
},
{
tab: "served",
icon: ServerIcon,
i18nKey: I18nKey.COMMON$APP,
},
{
tab: "browser",
icon: GlobeIcon,
i18nKey: I18nKey.COMMON$BROWSER,
},
{ tab: "editor", icon: GitChanges, i18nKey: I18nKey.COMMON$CHANGES },
{ tab: "vscode", icon: VSCodeIcon, i18nKey: I18nKey.COMMON$CODE },
{ tab: "terminal", icon: TerminalIcon, i18nKey: I18nKey.COMMON$TERMINAL },
{ tab: "served", icon: ServerIcon, i18nKey: I18nKey.COMMON$APP },
{ tab: "browser", icon: GlobeIcon, i18nKey: I18nKey.COMMON$BROWSER },
];
if (shouldUsePlanningAgent) {
@@ -71,30 +54,22 @@ export function ConversationTabsContextMenu({
if (!isOpen) return null;
const handleTabClick = (tab: string) => {
const tabString = tab;
if (unpinnedTabs.includes(tabString)) {
// Tab is unpinned, pin it (remove from unpinned list)
setUnpinnedTabs(
unpinnedTabs.filter((unpinnedTab) => unpinnedTab !== tabString),
);
} else {
// Tab is pinned, unpin it (add to unpinned list)
setUnpinnedTabs([...unpinnedTabs, tabString]);
}
setUnpinnedTabs((prev) =>
prev.includes(tab)
? prev.filter((tabItem) => tabItem !== tab)
: [...prev, tab],
);
};
const isTabPinned = (tab: string) => !unpinnedTabs.includes(tab as string);
return (
<ContextMenu
testId="conversation-tabs-context-menu"
ref={ref}
alignment="right"
position="bottom"
className="mt-2 w-fit z-[9999]"
>
{tabConfig.map(({ tab, icon: Icon, i18nKey }) => {
const pinned = isTabPinned(tab);
const pinned = !unpinnedTabs.includes(tab);
return (
<ContextMenuListItem
key={tab}
@@ -104,9 +79,9 @@ export function ConversationTabsContextMenu({
<Icon className="w-4 h-4" />
<span className="text-white text-sm">{t(i18nKey)}</span>
{pinned ? (
<PillFillIcon className="w-7 h-7 ml-auto flex-shrink-0 text-white -mr-[5px]" />
<PillFillIcon className="w-7 h-7 ml-auto -mr-[5px]" />
) : (
<PillIcon className="w-4.5 h-4.5 ml-auto flex-shrink-0 text-white" />
<PillIcon className="w-4.5 h-4.5 ml-auto" />
)}
</ContextMenuListItem>
);