mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-05 20:25:52 +00:00
fix: reset-state-on-new-session-click
This commit is contained in:
@@ -44,6 +44,7 @@ function AppContentInner() {
|
||||
sidebarOpen,
|
||||
isLoadingProjects,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
setActiveTab,
|
||||
setSidebarOpen,
|
||||
setIsInputFocused,
|
||||
@@ -194,6 +195,7 @@ function AppContentInner() {
|
||||
onNavigateToSession={(targetSessionId: string) => navigate(`/session/${targetSessionId}`)}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
externalMessageUpdate={externalMessageUpdate}
|
||||
newSessionTrigger={newSessionTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ interface UseChatSessionStateArgs {
|
||||
sendMessage: (message: unknown) => void;
|
||||
autoScrollToBottom?: boolean;
|
||||
externalMessageUpdate?: number;
|
||||
newSessionTrigger?: number;
|
||||
processingSessions?: Set<string>;
|
||||
resetStreamingState: () => void;
|
||||
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
||||
@@ -95,6 +96,7 @@ export function useChatSessionState({
|
||||
sendMessage,
|
||||
autoScrollToBottom,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
processingSessions,
|
||||
resetStreamingState,
|
||||
pendingViewSessionRef,
|
||||
@@ -131,9 +133,78 @@ export function useChatSessionState({
|
||||
const loadAllFinishedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const loadAllOverlayTimerRef = useRef<ReturnType<typeof setTimeout> | 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(), []);
|
||||
|
||||
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 */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
@@ -113,6 +113,7 @@ export interface ChatInterfaceProps {
|
||||
autoScrollToBottom?: boolean;
|
||||
sendByCtrlEnter?: boolean;
|
||||
externalMessageUpdate?: number;
|
||||
newSessionTrigger?: number;
|
||||
onTaskClick?: (...args: unknown[]) => void;
|
||||
onShowAllTasks?: (() => void) | null;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ function ChatInterface({
|
||||
autoScrollToBottom,
|
||||
sendByCtrlEnter,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
onShowAllTasks,
|
||||
}: ChatInterfaceProps) {
|
||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||
@@ -123,6 +124,7 @@ function ChatInterface({
|
||||
sendMessage,
|
||||
autoScrollToBottom,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
processingSessions,
|
||||
resetStreamingState,
|
||||
pendingViewSessionRef,
|
||||
|
||||
@@ -53,6 +53,7 @@ export type MainContentProps = {
|
||||
onNavigateToSession: (targetSessionId: string) => void;
|
||||
onShowSettings: () => void;
|
||||
externalMessageUpdate: number;
|
||||
newSessionTrigger: number;
|
||||
};
|
||||
|
||||
export type MainContentHeaderProps = {
|
||||
|
||||
@@ -51,6 +51,7 @@ function MainContent({
|
||||
onNavigateToSession,
|
||||
onShowSettings,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
}: MainContentProps) {
|
||||
const { preferences } = useUiPreferences();
|
||||
const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
|
||||
@@ -145,6 +146,7 @@ function MainContent({
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
externalMessageUpdate={externalMessageUpdate}
|
||||
newSessionTrigger={newSessionTrigger}
|
||||
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -261,6 +261,27 @@ export function useProjectsState({
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
|
||||
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 lastHandledMessageRef = useRef<AppSocketMessage | null>(null);
|
||||
@@ -587,6 +608,7 @@ export function useProjectsState({
|
||||
setSelectedProject(project);
|
||||
setSelectedSession(null);
|
||||
setActiveTab('chat');
|
||||
setNewSessionTrigger((previous) => previous + 1);
|
||||
navigate('/');
|
||||
|
||||
if (isMobile) {
|
||||
@@ -806,6 +828,7 @@ export function useProjectsState({
|
||||
showSettings,
|
||||
settingsInitialTab,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
setActiveTab,
|
||||
setSidebarOpen,
|
||||
setIsInputFocused,
|
||||
|
||||
Reference in New Issue
Block a user