From d1b43a42faabe824c26ebc571e41f1f081db504e Mon Sep 17 00:00:00 2001 From: litunan <97937065+litunan@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:59:09 +0800 Subject: [PATCH] fix(web-ui): isolate input history per session - Modify inputHistoryStore to store history by sessionId instead of global - Add sessionId parameter to addMessage, getMessage, getCount, getSessionHistory - Update ChatInput.tsx to use session-scoped history - Reset historyIndex when switching sessions - Add migration from old global format to new session-scoped format (v2) --- .../sections/sessions/SessionsSection.tsx | 6 +- .../src/flow_chat/components/ChatInput.tsx | 16 +++- .../src/flow_chat/store/inputHistoryStore.ts | 92 ++++++++++++++----- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 4e8e160b..d23227fe 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -26,7 +26,7 @@ const INACTIVE_WORKSPACE_EXPANDED_SESSIONS = 7; const log = createLogger('SessionsSection'); const AGENT_SCENE: SceneTabId = 'session'; -const resolveSessionMode = (session: Session): SessionMode => { +const resolveSessionModeType = (session: Session): SessionMode => { return session.mode?.toLowerCase() === 'cowork' ? 'cowork' : 'code'; }; @@ -139,7 +139,7 @@ const SessionsSection: React.FC = ({ const matched = rawTitle.match(/^(?:新建会话|New Session)\s*(\d+)$/i); if (!matched) return rawTitle; - const mode = resolveSessionMode(session); + const mode = resolveSessionModeType(session); const label = mode === 'cowork' ? t('nav.sessions.newCoworkSession') @@ -209,7 +209,7 @@ const SessionsSection: React.FC = ({ ) : ( visibleSessions.map(session => { const isEditing = editingSessionId === session.sessionId; - const sessionModeKey = resolveSessionMode(session); + const sessionModeKey = resolveSessionModeType(session); const sessionTitle = resolveSessionTitle(session); const SessionIcon = sessionModeKey === 'cowork' ? Users : Code2; const row = ( diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index c3b8c735..175daab8 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -65,7 +65,7 @@ export const ChatInput: React.FC = ({ // History navigation state const [historyIndex, setHistoryIndex] = useState(-1); const [savedDraft, setSavedDraft] = useState(''); - const { messages: inputHistory, addMessage: addToHistory } = useInputHistoryStore(); + const { addMessage: addToHistory, getSessionHistory } = useInputHistoryStore(); const contexts = useContextStore(state => state.contexts); const addContext = useContextStore(state => state.addContext); @@ -79,6 +79,9 @@ export const ChatInput: React.FC = ({ const activeSessionState = useActiveSessionState(); const currentSessionId = activeSessionState.sessionId; + + // Get input history for current session (after currentSessionId is defined) + const inputHistory = currentSessionId ? getSessionHistory(currentSessionId) : []; const derivedState = useSessionDerivedState(currentSessionId); const { transition, setQueuedInput } = useSessionStateMachineActions(currentSessionId); const stateMachine = useSessionStateMachine(currentSessionId); @@ -105,6 +108,11 @@ export const ChatInput: React.FC = ({ setChatInputExpanded(inputState.isExpanded); }, [inputState.isExpanded, setChatInputExpanded]); + // Reset history index when switching sessions + useEffect(() => { + setHistoryIndex(-1); + }, [currentSessionId]); + const { sendMessage } = useMessageSender({ currentSessionId: currentSessionId || undefined, contexts, @@ -611,8 +619,10 @@ export const ChatInput: React.FC = ({ const message = inputState.value.trim(); - // Add to history before clearing - addToHistory(message); + // Add to history before clearing (session-scoped) + if (currentSessionId) { + addToHistory(currentSessionId, message); + } setHistoryIndex(-1); setSavedDraft(''); diff --git a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts index ffe962df..c581c5b5 100644 --- a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts +++ b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts @@ -1,66 +1,110 @@ /** * Input history store for navigating previously sent messages. * Provides terminal-like up/down arrow navigation through message history. + * History is now session-scoped - each session maintains its own input history. */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; export interface InputHistoryState { - /** List of previously sent messages (most recent first) */ - messages: string[]; - /** Maximum number of messages to keep */ + /** Map of sessionId to list of previously sent messages (most recent first) */ + messagesBySession: Record; + /** Maximum number of messages to keep per session */ maxHistorySize: number; - /** Add a message to history */ - addMessage: (message: string) => void; - /** Clear all history */ - clearHistory: () => void; - /** Get message at index (0 = most recent) */ - getMessage: (index: number) => string | null; - /** Get total count */ - getCount: () => number; + /** Add a message to history for a specific session */ + addMessage: (sessionId: string, message: string) => void; + /** Clear history for a specific session */ + clearHistory: (sessionId?: string) => void; + /** Get message at index for a specific session (0 = most recent) */ + getMessage: (sessionId: string, index: number) => string | null; + /** Get total count for a specific session */ + getCount: (sessionId: string) => number; + /** Get all history for a specific session */ + getSessionHistory: (sessionId: string) => string[]; } export const useInputHistoryStore = create()( persist( (set, get) => ({ - messages: [], + messagesBySession: {}, maxHistorySize: 100, - addMessage: (message: string) => { + addMessage: (sessionId: string, message: string) => { const trimmed = message.trim(); - if (!trimmed) return; + if (!trimmed || !sessionId) return; set((state) => { + const sessionHistory = state.messagesBySession[sessionId] || []; + // Don't add duplicates in a row - if (state.messages[0] === trimmed) { + if (sessionHistory[0] === trimmed) { return state; } // Remove the message if it exists elsewhere in history - const filtered = state.messages.filter(m => m !== trimmed); + const filtered = sessionHistory.filter(m => m !== trimmed); // Add to front, limit size const newMessages = [trimmed, ...filtered].slice(0, state.maxHistorySize); - return { messages: newMessages }; + return { + messagesBySession: { + ...state.messagesBySession, + [sessionId]: newMessages + } + }; }); }, - clearHistory: () => set({ messages: [] }), + clearHistory: (sessionId?: string) => { + if (!sessionId) { + // Clear all history + set({ messagesBySession: {} }); + } else { + // Clear only specific session + set((state) => { + const newHistory = { ...state.messagesBySession }; + delete newHistory[sessionId]; + return { messagesBySession: newHistory }; + }); + } + }, + + getMessage: (sessionId: string, index: number) => { + const { messagesBySession } = get(); + const sessionHistory = messagesBySession[sessionId] || []; + if (index < 0 || index >= sessionHistory.length) return null; + return sessionHistory[index]; + }, - getMessage: (index: number) => { - const { messages } = get(); - if (index < 0 || index >= messages.length) return null; - return messages[index]; + getCount: (sessionId: string) => { + const { messagesBySession } = get(); + return (messagesBySession[sessionId] || []).length; }, - getCount: () => get().messages.length, + getSessionHistory: (sessionId: string) => { + const { messagesBySession } = get(); + return messagesBySession[sessionId] || []; + }, }), { name: 'bitfun-input-history', - version: 1, + version: 2, // Bump version to migrate from old format + migrate: (persistedState: any, version: number) => { + if (version < 2) { + // Migrate from old global format to new session-scoped format + // Old format: { messages: string[] } + // New format: { messagesBySession: Record } + return { + messagesBySession: {}, + maxHistorySize: persistedState.maxHistorySize || 100, + // Don't migrate old global history - users will start fresh + }; + } + return persistedState; + }, } ) );