mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-16 20:32:00 +08:00
fix: resolve session provider on backend reads
Session history and token usage reads already have a stable app session id. Passing provider and project hints from the frontend kept those reads coupled with provider-specific state that the backend can resolve from the session row. Resolve token usage provider server-side and narrow the session store read API to session id plus pagination. This keeps provider-specific storage decisions behind the backend boundary and makes reconnect, pagination, and load-all use the same session-owned contract.
This commit is contained in:
@@ -1135,7 +1135,6 @@ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req
|
||||
app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectId, sessionId } = req.params;
|
||||
const { provider = 'claude' } = req.query;
|
||||
const homeDir = os.homedir();
|
||||
|
||||
// Allow only safe characters in sessionId
|
||||
@@ -1146,8 +1145,14 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
||||
|
||||
// Provider artifacts on disk (JSONL file names, OpenCode sqlite rows)
|
||||
// are keyed by the provider-native session id, while the caller sends
|
||||
// the app-facing id. Resolve the mapping once for all branches below.
|
||||
// the app-facing id. Resolve provider and id mapping from the indexed
|
||||
// session row so the frontend does not choose provider-specific paths.
|
||||
const sessionRow = sessionsDb.getSessionById(safeSessionId);
|
||||
if (!sessionRow) {
|
||||
return res.status(404).json({ error: 'Session not found', sessionId: safeSessionId });
|
||||
}
|
||||
|
||||
const provider = sessionRow.provider || 'claude';
|
||||
const providerNativeSessionId = sessionRow?.provider_session_id || safeSessionId;
|
||||
|
||||
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
||||
|
||||
@@ -5,7 +5,7 @@ import { authenticatedFetch } from '../../../utils/api';
|
||||
import type { MarkSessionIdle, SessionActivityMap } from '../../../hooks/useSessionProtection';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
import type { ChatMessage, Provider } from '../types/types';
|
||||
import type { ChatMessage } from '../types/types';
|
||||
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
||||
|
||||
import { normalizedToChatMessages } from './useChatMessages';
|
||||
@@ -328,18 +328,12 @@ export function useChatSessionState({
|
||||
if (allMessagesLoadedRef.current) return false;
|
||||
if (!hasMoreMessages || !selectedSession || !selectedProject) return false;
|
||||
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
|
||||
isLoadingMoreRef.current = true;
|
||||
const previousScrollHeight = container.scrollHeight;
|
||||
const previousScrollTop = container.scrollTop;
|
||||
|
||||
try {
|
||||
const slot = await sessionStore.fetchMore(selectedSession.id, {
|
||||
provider: sessionProvider as LLMProvider,
|
||||
// DB-assigned projectId replaces the legacy folder-derived name.
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: MESSAGES_PER_PAGE,
|
||||
});
|
||||
if (!slot || slot.serverMessages.length === 0) return false;
|
||||
@@ -458,8 +452,7 @@ export function useChatSessionState({
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = (selectedSession.__provider || localStorage.getItem('selected-provider') as Provider) || 'claude';
|
||||
const sessionKey = `${selectedSession.id}:${selectedProject.projectId}:${provider}`;
|
||||
const sessionKey = `${selectedSession.id}:${selectedProject.projectId}`;
|
||||
|
||||
// Skip if already loaded and fresh
|
||||
if (lastLoadedSessionKeyRef.current === sessionKey && sessionStore.has(selectedSession.id) && !sessionStore.isStale(selectedSession.id)) {
|
||||
@@ -512,9 +505,6 @@ export function useChatSessionState({
|
||||
// Fetch from server → store updates → chatMessages re-derives automatically
|
||||
setIsLoadingSessionMessages(true);
|
||||
sessionStore.fetchFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: MESSAGES_PER_PAGE,
|
||||
offset: 0,
|
||||
}).then(slot => {
|
||||
@@ -544,15 +534,9 @@ export function useChatSessionState({
|
||||
|
||||
const reloadExternalMessages = async () => {
|
||||
try {
|
||||
const provider = (localStorage.getItem('selected-provider') as Provider) || 'claude';
|
||||
|
||||
// Skip store refresh during active streaming
|
||||
if (!isProcessing) {
|
||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
await sessionStore.refreshFromServer(selectedSession.id);
|
||||
|
||||
if (Boolean(autoScrollToBottom) && isNearBottom()) {
|
||||
setTimeout(() => scrollToBottom(), 200);
|
||||
@@ -598,13 +582,9 @@ export function useChatSessionState({
|
||||
|
||||
const scrollToTarget = async () => {
|
||||
if (!allMessagesLoadedRef.current && selectedSession && selectedProject) {
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
try {
|
||||
// Load all messages into the store for search navigation
|
||||
const slot = await sessionStore.fetchFromServer(selectedSession.id, {
|
||||
provider: sessionProvider as LLMProvider,
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: null,
|
||||
offset: 0,
|
||||
});
|
||||
@@ -678,13 +658,10 @@ export function useChatSessionState({
|
||||
setTokenBudget(null);
|
||||
return;
|
||||
}
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
|
||||
const fetchInitialTokenUsage = async () => {
|
||||
try {
|
||||
// Token usage endpoint is now keyed by the DB projectId.
|
||||
const params = new URLSearchParams({ provider: sessionProvider });
|
||||
const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage?${params.toString()}`;
|
||||
// The backend resolves the provider from the indexed session row.
|
||||
const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage`;
|
||||
const response = await authenticatedFetch(url);
|
||||
if (response.ok) {
|
||||
setTokenBudget(await response.json());
|
||||
@@ -696,7 +673,7 @@ export function useChatSessionState({
|
||||
}
|
||||
};
|
||||
fetchInitialTokenUsage();
|
||||
}, [selectedProject, selectedSession?.id, selectedSession?.__provider]);
|
||||
}, [selectedProject, selectedSession?.id]);
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
||||
@@ -756,8 +733,6 @@ export function useChatSessionState({
|
||||
const loadAllMessages = useCallback(async () => {
|
||||
if (!selectedSession || !selectedProject) return;
|
||||
if (isLoadingAllMessages) return;
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
|
||||
const requestSessionId = selectedSession.id;
|
||||
allMessagesLoadedRef.current = true;
|
||||
isLoadingMoreRef.current = true;
|
||||
@@ -770,9 +745,6 @@ export function useChatSessionState({
|
||||
|
||||
try {
|
||||
const slot = await sessionStore.fetchFromServer(requestSessionId, {
|
||||
provider: sessionProvider as LLMProvider,
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
limit: null,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useWebSocket } from '../../../contexts/WebSocketContext';
|
||||
import PermissionContext from '../../../contexts/PermissionContext';
|
||||
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
||||
import type { ChatInterfaceProps, Provider } from '../types/types';
|
||||
import type { LLMProvider } from '../../../types/app';
|
||||
import { useChatProviderState } from '../hooks/useChatProviderState';
|
||||
import { useChatSessionState } from '../hooks/useChatSessionState';
|
||||
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
||||
@@ -223,16 +222,7 @@ function ChatInterface({
|
||||
// missed live events, and re-attaches a still-running stream to this socket.
|
||||
const handleWebSocketReconnect = useCallback(async () => {
|
||||
if (!selectedProject || !selectedSession) return;
|
||||
const providerVal =
|
||||
selectedSession.__provider
|
||||
|| (localStorage.getItem('selected-provider') as LLMProvider)
|
||||
|| 'claude';
|
||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
||||
provider: providerVal as LLMProvider,
|
||||
// Use DB projectId; legacy folder-derived projectName is no longer accepted here.
|
||||
projectId: selectedProject.projectId,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
await sessionStore.refreshFromServer(selectedSession.id);
|
||||
statusCheckSentAtRef.current.set(selectedSession.id, Date.now());
|
||||
sendMessage({
|
||||
type: 'chat.subscribe',
|
||||
|
||||
@@ -454,9 +454,6 @@ export function useSessionStore() {
|
||||
const fetchFromServer = useCallback(async (
|
||||
sessionId: string,
|
||||
opts: {
|
||||
provider?: LLMProvider;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
limit?: number | null;
|
||||
offset?: number;
|
||||
} = {},
|
||||
@@ -511,9 +508,6 @@ export function useSessionStore() {
|
||||
const fetchMore = useCallback(async (
|
||||
sessionId: string,
|
||||
opts: {
|
||||
provider?: LLMProvider;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
limit?: number;
|
||||
} = {},
|
||||
) => {
|
||||
@@ -592,11 +586,6 @@ export function useSessionStore() {
|
||||
*/
|
||||
const refreshFromServer = useCallback(async (
|
||||
sessionId: string,
|
||||
_opts: {
|
||||
provider?: LLMProvider;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
} = {},
|
||||
) => {
|
||||
const slot = getSlot(sessionId);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user