mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-31 09:25:32 +08:00
fix: reset-state-on-new-session-click
This commit is contained in:
@@ -44,6 +44,7 @@ function AppContentInner() {
|
|||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
isLoadingProjects,
|
isLoadingProjects,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setSidebarOpen,
|
setSidebarOpen,
|
||||||
setIsInputFocused,
|
setIsInputFocused,
|
||||||
@@ -194,6 +195,7 @@ function AppContentInner() {
|
|||||||
onNavigateToSession={(targetSessionId: string) => navigate(`/session/${targetSessionId}`)}
|
onNavigateToSession={(targetSessionId: string) => navigate(`/session/${targetSessionId}`)}
|
||||||
onShowSettings={() => setShowSettings(true)}
|
onShowSettings={() => setShowSettings(true)}
|
||||||
externalMessageUpdate={externalMessageUpdate}
|
externalMessageUpdate={externalMessageUpdate}
|
||||||
|
newSessionTrigger={newSessionTrigger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface UseChatSessionStateArgs {
|
|||||||
sendMessage: (message: unknown) => void;
|
sendMessage: (message: unknown) => void;
|
||||||
autoScrollToBottom?: boolean;
|
autoScrollToBottom?: boolean;
|
||||||
externalMessageUpdate?: number;
|
externalMessageUpdate?: number;
|
||||||
|
newSessionTrigger?: number;
|
||||||
processingSessions?: Set<string>;
|
processingSessions?: Set<string>;
|
||||||
resetStreamingState: () => void;
|
resetStreamingState: () => void;
|
||||||
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
||||||
@@ -95,6 +96,7 @@ export function useChatSessionState({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
autoScrollToBottom,
|
autoScrollToBottom,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
processingSessions,
|
processingSessions,
|
||||||
resetStreamingState,
|
resetStreamingState,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
@@ -131,9 +133,78 @@ export function useChatSessionState({
|
|||||||
const loadAllFinishedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const loadAllFinishedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const loadAllOverlayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const loadAllOverlayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastLoadedSessionKeyRef = useRef<string | null>(null);
|
const lastLoadedSessionKeyRef = useRef<string | null>(null);
|
||||||
|
/**
|
||||||
|
* Tracks the last processed value from `useProjectsState.newSessionTrigger`.
|
||||||
|
*
|
||||||
|
* The trigger itself is intentionally increment-only and routed via:
|
||||||
|
* useProjectsState -> AppContent -> MainContent -> ChatInterface -> this hook.
|
||||||
|
* We compare values to ensure each explicit New Session click runs exactly one
|
||||||
|
* reset pass in this local chat state domain.
|
||||||
|
*/
|
||||||
|
const previousNewSessionTriggerRef = useRef(newSessionTrigger ?? 0);
|
||||||
|
|
||||||
const createDiff = useMemo<DiffCalculator>(() => createCachedDiffCalculator(), []);
|
const createDiff = useMemo<DiffCalculator>(() => createCachedDiffCalculator(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trigger = newSessionTrigger ?? 0;
|
||||||
|
if (trigger === previousNewSessionTriggerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previousNewSessionTriggerRef.current = trigger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumer-side reset for explicit New Session intent.
|
||||||
|
*
|
||||||
|
* Why this is essential:
|
||||||
|
* - Chat keeps local state that is not fully derived from `selectedSession`:
|
||||||
|
* `currentSessionId`, `pendingUserMessage`, streaming/status flags, message
|
||||||
|
* pagination/scroll bookkeeping, and pending session IDs in sessionStorage.
|
||||||
|
* - If the user clicks New Session while already on the same route with no
|
||||||
|
* selected session, parent state updates can be idempotent and this local
|
||||||
|
* state would otherwise persist, making the click appear to "do nothing".
|
||||||
|
*
|
||||||
|
* What this reset guarantees:
|
||||||
|
* - A deterministic clean draft state on every New Session click.
|
||||||
|
* - No dependence on route/tab/session-object identity changes.
|
||||||
|
* - No coupling to unrelated external update signals.
|
||||||
|
*/
|
||||||
|
resetStreamingState();
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
|
setClaudeStatus(null);
|
||||||
|
setCanAbortSession(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
setCurrentSessionId(null);
|
||||||
|
setPendingUserMessage(null);
|
||||||
|
sessionStorage.removeItem('pendingSessionId');
|
||||||
|
sessionStorage.removeItem('cursorSessionId');
|
||||||
|
messagesOffsetRef.current = 0;
|
||||||
|
setHasMoreMessages(false);
|
||||||
|
setTotalMessages(0);
|
||||||
|
setTokenBudget(null);
|
||||||
|
setVisibleMessageCount(INITIAL_VISIBLE_MESSAGES);
|
||||||
|
setAllMessagesLoaded(false);
|
||||||
|
allMessagesLoadedRef.current = false;
|
||||||
|
setIsLoadingAllMessages(false);
|
||||||
|
setLoadAllJustFinished(false);
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
setViewHiddenCount(0);
|
||||||
|
setSearchTarget(null);
|
||||||
|
searchScrollActiveRef.current = false;
|
||||||
|
topLoadLockRef.current = false;
|
||||||
|
pendingScrollRestoreRef.current = null;
|
||||||
|
pendingInitialScrollRef.current = true;
|
||||||
|
lastLoadedSessionKeyRef.current = null;
|
||||||
|
|
||||||
|
if (loadAllOverlayTimerRef.current) {
|
||||||
|
clearTimeout(loadAllOverlayTimerRef.current);
|
||||||
|
loadAllOverlayTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (loadAllFinishedTimerRef.current) {
|
||||||
|
clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
|
loadAllFinishedTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, [newSessionTrigger, pendingViewSessionRef, resetStreamingState]);
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- */
|
/* ---------------------------------------------------------------- */
|
||||||
/* Derive chatMessages from the store */
|
/* Derive chatMessages from the store */
|
||||||
/* ---------------------------------------------------------------- */
|
/* ---------------------------------------------------------------- */
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export interface ChatInterfaceProps {
|
|||||||
autoScrollToBottom?: boolean;
|
autoScrollToBottom?: boolean;
|
||||||
sendByCtrlEnter?: boolean;
|
sendByCtrlEnter?: boolean;
|
||||||
externalMessageUpdate?: number;
|
externalMessageUpdate?: number;
|
||||||
|
newSessionTrigger?: number;
|
||||||
onTaskClick?: (...args: unknown[]) => void;
|
onTaskClick?: (...args: unknown[]) => void;
|
||||||
onShowAllTasks?: (() => void) | null;
|
onShowAllTasks?: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function ChatInterface({
|
|||||||
autoScrollToBottom,
|
autoScrollToBottom,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
onShowAllTasks,
|
onShowAllTasks,
|
||||||
}: ChatInterfaceProps) {
|
}: ChatInterfaceProps) {
|
||||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||||
@@ -123,6 +124,7 @@ function ChatInterface({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
autoScrollToBottom,
|
autoScrollToBottom,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
processingSessions,
|
processingSessions,
|
||||||
resetStreamingState,
|
resetStreamingState,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type MainContentProps = {
|
|||||||
onNavigateToSession: (targetSessionId: string) => void;
|
onNavigateToSession: (targetSessionId: string) => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
externalMessageUpdate: number;
|
externalMessageUpdate: number;
|
||||||
|
newSessionTrigger: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MainContentHeaderProps = {
|
export type MainContentHeaderProps = {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function MainContent({
|
|||||||
onNavigateToSession,
|
onNavigateToSession,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
}: MainContentProps) {
|
}: MainContentProps) {
|
||||||
const { preferences } = useUiPreferences();
|
const { preferences } = useUiPreferences();
|
||||||
const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
|
const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
|
||||||
@@ -145,6 +146,7 @@ function MainContent({
|
|||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
sendByCtrlEnter={sendByCtrlEnter}
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
externalMessageUpdate={externalMessageUpdate}
|
externalMessageUpdate={externalMessageUpdate}
|
||||||
|
newSessionTrigger={newSessionTrigger}
|
||||||
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -261,6 +261,27 @@ export function useProjectsState({
|
|||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
|
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
|
||||||
const [externalMessageUpdate, setExternalMessageUpdate] = useState(0);
|
const [externalMessageUpdate, setExternalMessageUpdate] = useState(0);
|
||||||
|
/**
|
||||||
|
* `newSessionTrigger` is an explicit, monotonic intent signal for user-driven
|
||||||
|
* New Session actions.
|
||||||
|
*
|
||||||
|
* It exists because `handleNewSession` can be invoked while the app is already in
|
||||||
|
* the same visible state (`selectedSession === null`, `activeTab === 'chat'`,
|
||||||
|
* route already `/`). In that case, React/router updates are idempotent and no
|
||||||
|
* downstream reset logic runs.
|
||||||
|
*
|
||||||
|
* Usage across the codebase:
|
||||||
|
* 1) Produced here in `handleNewSession` via increment (always changes).
|
||||||
|
* 2) Returned from this hook and threaded through:
|
||||||
|
* useProjectsState -> AppContent -> MainContent -> ChatInterface.
|
||||||
|
* 3) Consumed in `useChatSessionState` as an effect dependency to forcibly clear
|
||||||
|
* chat-local state (`currentSessionId`, pending draft message, streaming flags,
|
||||||
|
* pending session storage keys, pagination/scroll artifacts).
|
||||||
|
*
|
||||||
|
* Keeping this signal dedicated avoids coupling resets to unrelated counters/events
|
||||||
|
* (for example websocket/project refresh updates) that could cause accidental resets.
|
||||||
|
*/
|
||||||
|
const [newSessionTrigger, setNewSessionTrigger] = useState(0);
|
||||||
|
|
||||||
const loadingProgressTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const loadingProgressTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastHandledMessageRef = useRef<AppSocketMessage | null>(null);
|
const lastHandledMessageRef = useRef<AppSocketMessage | null>(null);
|
||||||
@@ -587,6 +608,7 @@ export function useProjectsState({
|
|||||||
setSelectedProject(project);
|
setSelectedProject(project);
|
||||||
setSelectedSession(null);
|
setSelectedSession(null);
|
||||||
setActiveTab('chat');
|
setActiveTab('chat');
|
||||||
|
setNewSessionTrigger((previous) => previous + 1);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@@ -806,6 +828,7 @@ export function useProjectsState({
|
|||||||
showSettings,
|
showSettings,
|
||||||
settingsInitialTab,
|
settingsInitialTab,
|
||||||
externalMessageUpdate,
|
externalMessageUpdate,
|
||||||
|
newSessionTrigger,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setSidebarOpen,
|
setSidebarOpen,
|
||||||
setIsInputFocused,
|
setIsInputFocused,
|
||||||
|
|||||||
Reference in New Issue
Block a user