🐛 fix: fixed the pinned session not work (#10323)

* fix: fixed the pinned session not work

* feat: add urlHydration store to slove the url sync problem
This commit is contained in:
Shinji-Li
2025-11-24 10:46:10 +08:00
committed by GitHub
parent f8a24d22e3
commit 224f9998df
9 changed files with 158 additions and 21 deletions

View File

@@ -2,11 +2,12 @@ import { ActionIcon, ActionIconProps, Hotkey } from '@lobehub/ui';
import { Compass, FolderClosed, MessageSquare, Palette } from 'lucide-react';
import { memo, useMemo, useTransition } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { useNavigate } from 'react-router-dom';
import { INBOX_SESSION_ID } from '@/const/session';
import { SESSION_CHAT_URL } from '@/const/url';
import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
import { useGlobalStore } from '@/store/global';
import { SidebarTabKey } from '@/store/global/initialState';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -31,11 +32,8 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
const { t } = useTranslation('common');
const navigate = useNavigate();
const [, startTransition] = useTransition();
const [switchBackToChat, isMobile] = useGlobalStore((s) => [
s.switchBackToChat,
s.isMobile,
]);
const [, { unpinAgent }] = usePinnedAgentState();
const [switchBackToChat, isMobile] = useGlobalStore((s) => [s.switchBackToChat, s.isMobile]);
const { showMarket, enableKnowledgeBase, showAiImage } =
useServerConfigStore(featureFlagsSelectors);
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
@@ -69,6 +67,7 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
e.preventDefault();
startTransition(() => {
switchBackToChat(activeSessionId);
unpinAgent();
});
}}
size={ICON_SIZE}

View File

@@ -1,21 +1,28 @@
import { useMemo } from 'react';
'use client';
import { parseAsBoolean, useQueryParam } from './useQueryParam';
import { useSessionStore } from '@/store/session';
import { useUrlHydrationStore } from '@/store/urlHydration';
export const usePinnedAgentState = () => {
const [isPinned, setIsPinned] = useQueryParam('pinned', parseAsBoolean.withDefault(false), {
clearOnDefault: true,
});
const isPinned = useSessionStore((s) => s.isAgentPinned);
const setAgentPinned = useSessionStore((s) => s.setAgentPinned);
const toggleAgentPinned = useSessionStore((s) => s.toggleAgentPinned);
const syncToUrl = useUrlHydrationStore((s) => s.syncAgentPinnedToUrl);
const actions = useMemo(
() => ({
pinAgent: () => setIsPinned(true),
setIsPinned,
togglePinAgent: () => setIsPinned((prev) => !prev),
unpinAgent: () => setIsPinned(false),
}),
[setIsPinned],
);
const withSync = <T extends (...args: any[]) => void>(fn: T) => {
return (...args: Parameters<T>) => {
fn(...args);
syncToUrl();
};
};
return [isPinned, actions] as const;
return [
isPinned,
{
pinAgent: withSync(() => setAgentPinned(true)),
setIsPinned: withSync(setAgentPinned),
togglePinAgent: withSync(toggleAgentPinned),
unpinAgent: withSync(() => setAgentPinned(false)),
},
] as const;
};

View File

@@ -12,6 +12,7 @@ import { useAiInfraStore } from '@/store/aiInfra';
import { useGlobalStore } from '@/store/global';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import { useUrlHydrationStore } from '@/store/urlHydration';
import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
@@ -19,6 +20,10 @@ const StoreInitialization = memo(() => {
// prefetch error ns to avoid don't show error content correctly
useTranslation('error');
// Initialize from URL (one-time)
const initAgentPinnedFromUrl = useUrlHydrationStore((s) => s.initAgentPinnedFromUrl);
initAgentPinnedFromUrl();
const router = useRouter();
const [isLogin, isSignedIn, useInitUserState] = useUserStore((s) => [
authSelectors.isLogin(s),

View File

@@ -76,6 +76,15 @@ export interface SessionAction {
*/
removeSession: (id: string) => Promise<void>;
/**
* Set the agent panel pinned state
*/
setAgentPinned: (pinned: boolean | ((prev: boolean) => boolean)) => void;
/**
* Toggle the agent panel pinned state
*/
toggleAgentPinned: () => void;
updateSearchKeywords: (keywords: string) => void;
useFetchSessions: (
@@ -187,12 +196,26 @@ export const createSessionSlice: StateCreator<
}
},
setAgentPinned: (value) => {
set(
(state) => ({
isAgentPinned: typeof value === 'function' ? value(state.isAgentPinned) : value,
}),
false,
n('setAgentPinned'),
);
},
switchSession: (sessionId) => {
if (get().activeId === sessionId) return;
set({ activeId: sessionId }, false, n(`activeSession/${sessionId}`));
},
toggleAgentPinned: () => {
set((state) => ({ isAgentPinned: !state.isAgentPinned }), false, n('toggleAgentPinned'));
},
triggerSessionUpdate: async (id) => {
await get().internal_updateSession(id, { updatedAt: new Date() });
},

View File

@@ -7,6 +7,11 @@ export interface SessionState {
*/
activeId: string;
defaultSessions: LobeSessions;
/**
* @title Whether the agent panel is pinned
* @description Controls the agent panel pinning state in the UI layout
*/
isAgentPinned: boolean;
isSearching: boolean;
isSessionsFirstFetchFinished: boolean;
pinnedSessions: LobeSessions;
@@ -22,6 +27,7 @@ export interface SessionState {
export const initialSessionState: SessionState = {
activeId: 'inbox',
defaultSessions: [],
isAgentPinned: false,
isSearching: false,
isSessionsFirstFetchFinished: false,
pinnedSessions: [],

View File

@@ -0,0 +1,56 @@
import { StateCreator } from 'zustand/vanilla';
import { useSessionStore } from '@/store/session';
import type { UrlHydrationStore } from './store';
export interface UrlHydrationAction {
/**
* Initialize store state from URL (one-time on app load)
*/
initAgentPinnedFromUrl: () => void;
/**
* Sync agent pinned state to URL (call after state change)
*/
syncAgentPinnedToUrl: () => void;
}
export const urlHydrationAction: StateCreator<
UrlHydrationStore,
[['zustand/devtools', never]],
[],
UrlHydrationAction
> = (set, get) => ({
initAgentPinnedFromUrl: () => {
if (get().isAgentPinnedInitialized) return;
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
const pinnedParam = params.get('pinned');
console.log('pinnedParam', pinnedParam);
if (pinnedParam === 'true') {
useSessionStore.setState({ isAgentPinned: true });
}
set({ isAgentPinnedInitialized: true });
}
},
syncAgentPinnedToUrl: () => {
if (typeof window === 'undefined') return;
const isAgentPinned = useSessionStore.getState().isAgentPinned;
const url = new URL(window.location.href);
if (isAgentPinned) {
url.searchParams.set('pinned', 'true');
} else {
url.searchParams.delete('pinned');
}
window.history.replaceState(null, '', `${url.pathname}${url.search}`);
},
});

View File

@@ -0,0 +1 @@
export { type UrlHydrationStore,useUrlHydrationStore } from './store';

View File

@@ -0,0 +1,12 @@
/**
* URL Hydration Store State
*
* Tracks initialization status to ensure one-time URL reading.
*/
export interface UrlHydrationState {
isAgentPinnedInitialized: boolean;
}
export const initialState: UrlHydrationState = {
isAgentPinnedInitialized: false,
};

View File

@@ -0,0 +1,28 @@
import { subscribeWithSelector } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { StateCreator } from 'zustand/vanilla';
import { createDevtools } from '../middleware/createDevtools';
import { type UrlHydrationAction, urlHydrationAction } from './action';
import { type UrlHydrationState, initialState } from './initialState';
// =============== 聚合 createStoreFn ============ //
export interface UrlHydrationStore extends UrlHydrationState, UrlHydrationAction {}
const createStore: StateCreator<UrlHydrationStore, [['zustand/devtools', never]]> = (
...parameters
) => ({
...initialState,
...urlHydrationAction(...parameters),
});
// =============== 实装 useStore ============ //
const devtools = createDevtools('urlHydration');
export const useUrlHydrationStore = createWithEqualityFn<UrlHydrationStore>()(
subscribeWithSelector(devtools(createStore)),
shallow,
);