fix: correct notification session id

This commit is contained in:
Haileyesus
2026-06-11 19:31:13 +03:00
parent f5eac2ec12
commit 881e72d4a0
9 changed files with 254 additions and 22 deletions

View File

@@ -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}

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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}

View File

@@ -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,