mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-12 17:12:06 +08:00
fix: correct notification session id
This commit is contained in:
@@ -46,6 +46,7 @@ function AppContentInner() {
|
||||
setShowSettings,
|
||||
openSettings,
|
||||
refreshProjectsSilently,
|
||||
registerOptimisticSession,
|
||||
sidebarSharedProps,
|
||||
handleNewSession,
|
||||
} = useProjectsState({
|
||||
@@ -172,6 +173,9 @@ function AppContentInner() {
|
||||
onNavigateToSession={(targetSessionId: string, options) =>
|
||||
navigate(`/session/${targetSessionId}`, { replace: Boolean(options?.replace) })
|
||||
}
|
||||
onSessionEstablished={(targetSessionId, context) =>
|
||||
registerOptimisticSession({ sessionId: targetSessionId, ...context })
|
||||
}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
externalMessageUpdate={externalMessageUpdate}
|
||||
newSessionTrigger={newSessionTrigger}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ChatMessage,
|
||||
PendingPermissionRequest,
|
||||
PermissionMode,
|
||||
SessionEstablishedContext,
|
||||
} from '../types/types';
|
||||
import type { Project, ProjectSession, LLMProvider, ProviderModelsCacheInfo } from '../../../types/app';
|
||||
import { escapeRegExp } from '../utils/chatFormatting';
|
||||
@@ -51,7 +52,7 @@ interface UseChatComposerStateArgs {
|
||||
* stable for the conversation's whole lifetime — the consumer navigates to
|
||||
* /session/:id and records it as the current session.
|
||||
*/
|
||||
onSessionEstablished?: (sessionId: string) => void;
|
||||
onSessionEstablished?: (sessionId: string, context: SessionEstablishedContext) => void;
|
||||
onInputFocusChange?: (focused: boolean) => void;
|
||||
onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
|
||||
onShowSettings?: () => void;
|
||||
@@ -606,6 +607,7 @@ export function useChatComposerState({
|
||||
}
|
||||
|
||||
const resolvedProjectPath = selectedProject.fullPath || selectedProject.path || '';
|
||||
const sessionSummary = getNotificationSessionSummary(selectedSession, currentInput);
|
||||
|
||||
// The conversation always has a stable backend-allocated session id
|
||||
// BEFORE the first websocket send: brand-new chats allocate one here
|
||||
@@ -646,7 +648,11 @@ export function useChatComposerState({
|
||||
return;
|
||||
}
|
||||
|
||||
onSessionEstablished?.(targetSessionId);
|
||||
onSessionEstablished?.(targetSessionId, {
|
||||
provider,
|
||||
project: selectedProject,
|
||||
summary: sessionSummary,
|
||||
});
|
||||
}
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
@@ -696,8 +702,6 @@ export function useChatComposerState({
|
||||
};
|
||||
|
||||
const toolsSettings = getToolsSettings();
|
||||
const sessionSummary = getNotificationSessionSummary(selectedSession, currentInput);
|
||||
|
||||
const model =
|
||||
provider === 'cursor'
|
||||
? cursorModel
|
||||
|
||||
@@ -107,6 +107,12 @@ export type SessionNavigationOptions = {
|
||||
replace?: boolean;
|
||||
};
|
||||
|
||||
export type SessionEstablishedContext = {
|
||||
provider: LLMProvider;
|
||||
project: Project;
|
||||
summary?: string | null;
|
||||
};
|
||||
|
||||
export interface ChatInterfaceProps {
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
@@ -118,6 +124,7 @@ export interface ChatInterfaceProps {
|
||||
onSessionIdle?: MarkSessionIdle;
|
||||
processingSessions?: SessionActivityMap;
|
||||
onNavigateToSession?: (targetSessionId: string, options?: SessionNavigationOptions) => void;
|
||||
onSessionEstablished?: (sessionId: string, context: SessionEstablishedContext) => void;
|
||||
onShowSettings?: () => void;
|
||||
autoExpandTools?: boolean;
|
||||
showRawParameters?: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@ function ChatInterface({
|
||||
onSessionIdle,
|
||||
processingSessions,
|
||||
onNavigateToSession,
|
||||
onSessionEstablished,
|
||||
onShowSettings,
|
||||
autoExpandTools,
|
||||
showRawParameters,
|
||||
@@ -138,10 +139,11 @@ function ChatInterface({
|
||||
// Brand-new conversation: the composer allocated a stable session id via
|
||||
// the session gateway before the first send. Record it locally and put it
|
||||
// in the URL — this id never changes again, so there is no later handoff.
|
||||
const handleSessionEstablished = useCallback((sessionId: string) => {
|
||||
const handleSessionEstablished = useCallback<NonNullable<ChatInterfaceProps['onSessionEstablished']>>((sessionId, context) => {
|
||||
setCurrentSessionId(sessionId);
|
||||
onSessionEstablished?.(sessionId, context);
|
||||
onNavigateToSession?.(sessionId);
|
||||
}, [setCurrentSessionId, onNavigateToSession]);
|
||||
}, [setCurrentSessionId, onSessionEstablished, onNavigateToSession]);
|
||||
|
||||
const {
|
||||
input,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
MarkSessionProcessing,
|
||||
SessionActivityMap,
|
||||
} from '../../../hooks/useSessionProtection';
|
||||
import type { SessionNavigationOptions } from '../../chat/types/types';
|
||||
import type { SessionEstablishedContext, SessionNavigationOptions } from '../../chat/types/types';
|
||||
|
||||
export type TaskMasterTask = {
|
||||
id: string | number;
|
||||
@@ -52,6 +52,7 @@ export type MainContentProps = {
|
||||
onSessionIdle: MarkSessionIdle;
|
||||
processingSessions: SessionActivityMap;
|
||||
onNavigateToSession: (targetSessionId: string, options?: SessionNavigationOptions) => void;
|
||||
onSessionEstablished: (sessionId: string, context: SessionEstablishedContext) => void;
|
||||
onShowSettings: () => void;
|
||||
externalMessageUpdate: number;
|
||||
newSessionTrigger: number;
|
||||
|
||||
@@ -45,6 +45,7 @@ function MainContent({
|
||||
onSessionIdle,
|
||||
processingSessions,
|
||||
onNavigateToSession,
|
||||
onSessionEstablished,
|
||||
onShowSettings,
|
||||
externalMessageUpdate,
|
||||
newSessionTrigger,
|
||||
@@ -131,6 +132,7 @@ function MainContent({
|
||||
onSessionIdle={onSessionIdle}
|
||||
processingSessions={processingSessions}
|
||||
onNavigateToSession={onNavigateToSession}
|
||||
onSessionEstablished={onSessionEstablished}
|
||||
onShowSettings={onShowSettings}
|
||||
autoExpandTools={autoExpandTools}
|
||||
showRawParameters={showRawParameters}
|
||||
|
||||
@@ -44,6 +44,13 @@ type FetchProjectsOptions = {
|
||||
showLoadingState?: boolean;
|
||||
};
|
||||
|
||||
type RegisterOptimisticSessionArgs = {
|
||||
sessionId: string;
|
||||
provider: LLMProvider;
|
||||
project: Project;
|
||||
summary?: string | null;
|
||||
};
|
||||
|
||||
const serialize = (value: unknown) => JSON.stringify(value ?? null);
|
||||
|
||||
const projectsHaveChanges = (
|
||||
@@ -258,6 +265,21 @@ const upsertSessionIntoProject = (project: Project, event: SessionUpsertedEvent)
|
||||
return next;
|
||||
};
|
||||
|
||||
const projectFromRegistration = (project: Project): Project => ({
|
||||
projectId: project.projectId,
|
||||
path: project.path || project.fullPath,
|
||||
fullPath: project.fullPath || project.path || '',
|
||||
displayName: project.displayName,
|
||||
isStarred: project.isStarred,
|
||||
sessions: project.sessions ?? [],
|
||||
cursorSessions: project.cursorSessions ?? [],
|
||||
codexSessions: project.codexSessions ?? [],
|
||||
geminiSessions: project.geminiSessions ?? [],
|
||||
opencodeSessions: project.opencodeSessions ?? [],
|
||||
sessionMeta: project.sessionMeta ?? { hasMore: false, total: countLoadedProjectSessions(project) },
|
||||
taskmaster: project.taskmaster,
|
||||
});
|
||||
|
||||
const VALID_TABS: Set<string> = new Set(['chat', 'files', 'shell', 'git', 'tasks', 'preview']);
|
||||
|
||||
const isValidTab = (tab: string): tab is AppTab => {
|
||||
@@ -373,6 +395,75 @@ export function useProjectsState({
|
||||
await fetchProjects({ showLoadingState: false });
|
||||
}, [fetchProjects]);
|
||||
|
||||
const registerOptimisticSession = useCallback(({
|
||||
sessionId: newSessionId,
|
||||
provider,
|
||||
project,
|
||||
summary,
|
||||
}: RegisterOptimisticSessionArgs) => {
|
||||
if (!newSessionId || !project?.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const optimisticSession: ProjectSession = {
|
||||
id: newSessionId,
|
||||
summary: summary ?? '',
|
||||
messageCount: 0,
|
||||
createdAt: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
lastActivity: now,
|
||||
__provider: provider,
|
||||
__projectId: project.projectId,
|
||||
};
|
||||
const upsert: SessionUpsertedEvent = {
|
||||
kind: 'session_upserted',
|
||||
sessionId: newSessionId,
|
||||
provider,
|
||||
session: optimisticSession,
|
||||
project: {
|
||||
projectId: project.projectId,
|
||||
path: project.path || project.fullPath,
|
||||
fullPath: project.fullPath || project.path || '',
|
||||
displayName: project.displayName,
|
||||
isStarred: Boolean(project.isStarred),
|
||||
},
|
||||
timestamp: now,
|
||||
};
|
||||
|
||||
setProjects((previousProjects) => {
|
||||
const existingProject = previousProjects.find((candidate) => candidate.projectId === project.projectId);
|
||||
if (!existingProject) {
|
||||
return [upsertSessionIntoProject(projectFromRegistration(project), upsert), ...previousProjects];
|
||||
}
|
||||
|
||||
const updatedProject = upsertSessionIntoProject(existingProject, upsert);
|
||||
if (updatedProject === existingProject) {
|
||||
return previousProjects;
|
||||
}
|
||||
|
||||
return previousProjects.map((candidate) =>
|
||||
candidate.projectId === existingProject.projectId ? updatedProject : candidate,
|
||||
);
|
||||
});
|
||||
|
||||
setSelectedProject((previousProject) => {
|
||||
if (!previousProject || previousProject.projectId !== project.projectId) {
|
||||
return previousProject;
|
||||
}
|
||||
|
||||
const updatedProject = upsertSessionIntoProject(previousProject, upsert);
|
||||
return updatedProject === previousProject ? previousProject : updatedProject;
|
||||
});
|
||||
|
||||
setSelectedSession((previousSession) => (
|
||||
previousSession?.id === newSessionId
|
||||
? { ...previousSession, ...optimisticSession }
|
||||
: optimisticSession
|
||||
));
|
||||
}, []);
|
||||
|
||||
// Hydrates TaskMaster details for the given `projectId`. The project
|
||||
// identifier comes directly from the DB-driven /api/projects response.
|
||||
const hydrateProjectTaskMaster = useCallback(async (projectId: string) => {
|
||||
@@ -950,6 +1041,7 @@ export function useProjectsState({
|
||||
openSettings,
|
||||
fetchProjects,
|
||||
refreshProjectsSilently,
|
||||
registerOptimisticSession,
|
||||
sidebarSharedProps,
|
||||
handleProjectSelect,
|
||||
handleSessionSelect,
|
||||
|
||||
Reference in New Issue
Block a user