fix(ui): sync pin/unpin state across conversation tabs (#12884) (#12932)

Co-authored-by: hieptl <hieptl.developer@gmail.com>
This commit is contained in:
bendarte
2026-03-03 10:33:28 +01:00
committed by GitHub
parent 0c7ce4ad48
commit a7a4eb2664
2 changed files with 89 additions and 2 deletions

View File

@@ -88,6 +88,30 @@ describe("ConversationTabs localStorage behavior", () => {
const parsed = JSON.parse(storedState!);
expect(parsed.unpinnedTabs).toContain("terminal");
});
it("should hide a tab after unpinning it from context menu", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
render(
<>
<ConversationTabs />
<ConversationTabsContextMenu isOpen={true} onClose={vi.fn()} />
</>,
{ wrapper: createWrapper(REAL_CONVERSATION_ID) },
);
expect(
screen.getByTestId("conversation-tab-terminal"),
).toBeInTheDocument();
const terminalItem = screen.getByText("COMMON$TERMINAL");
await user.click(terminalItem);
expect(
screen.queryByTestId("conversation-tab-terminal"),
).not.toBeInTheDocument();
});
});
describe("hook integration", () => {

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import type {
ConversationTab,
ConversationMode,
@@ -8,6 +8,12 @@ export const LOCAL_STORAGE_KEYS = {
CONVERSATION_STATE: "conversation-state",
} as const;
const CONVERSATION_STATE_UPDATED_EVENT = "conversation-state-updated";
type ConversationStateUpdatedDetail = {
conversationId: string;
};
/**
* Consolidated conversation state stored in a single localStorage key.
*/
@@ -71,6 +77,14 @@ export function setConversationState(
const currentState = getConversationState(conversationId);
const newState = { ...currentState, ...updates };
localStorage.setItem(key, JSON.stringify(newState));
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent<ConversationStateUpdatedDetail>(
CONVERSATION_STATE_UPDATED_EVENT,
{ detail: { conversationId } },
),
);
}
} catch (err) {
console.warn("Failed to set conversation localStorage", err);
}
@@ -80,6 +94,14 @@ export function clearConversationLocalStorage(conversationId: string) {
try {
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
localStorage.removeItem(key);
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent<ConversationStateUpdatedDetail>(
CONVERSATION_STATE_UPDATED_EVENT,
{ detail: { conversationId } },
),
);
}
} catch (err) {
console.warn(
"Failed to clear conversation localStorage",
@@ -104,8 +126,49 @@ export function useConversationLocalStorageState(conversationId: string): {
getConversationState(conversationId),
);
useEffect(() => {
if (typeof window === "undefined") {
return undefined;
}
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
const syncState = () => {
setState(getConversationState(conversationId));
};
const handleStorage = (event: StorageEvent) => {
if (event.key === key) {
syncState();
}
};
const handleConversationStateUpdated = (event: Event) => {
const customEvent = event as CustomEvent<ConversationStateUpdatedDetail>;
if (customEvent.detail?.conversationId === conversationId) {
syncState();
}
};
// Ensure this hook reflects latest state for the current conversation ID.
syncState();
window.addEventListener("storage", handleStorage);
window.addEventListener(
CONVERSATION_STATE_UPDATED_EVENT,
handleConversationStateUpdated,
);
return () => {
window.removeEventListener("storage", handleStorage);
window.removeEventListener(
CONVERSATION_STATE_UPDATED_EVENT,
handleConversationStateUpdated,
);
};
}, [conversationId]);
const updateState = (updates: Partial<ConversationState>) => {
setState((prev) => ({ ...prev, ...updates }));
setConversationState(conversationId, updates);
};