mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 15:25:27 +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) => {
|
app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { projectId, sessionId } = req.params;
|
const { projectId, sessionId } = req.params;
|
||||||
const { provider = 'claude' } = req.query;
|
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
|
|
||||||
// Allow only safe characters in sessionId
|
// 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)
|
// Provider artifacts on disk (JSONL file names, OpenCode sqlite rows)
|
||||||
// are keyed by the provider-native session id, while the caller sends
|
// 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);
|
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;
|
const providerNativeSessionId = sessionRow?.provider_session_id || safeSessionId;
|
||||||
|
|
||||||
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
// 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 { MarkSessionIdle, SessionActivityMap } from '../../../hooks/useSessionProtection';
|
||||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
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 { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
||||||
|
|
||||||
import { normalizedToChatMessages } from './useChatMessages';
|
import { normalizedToChatMessages } from './useChatMessages';
|
||||||
@@ -328,18 +328,12 @@ export function useChatSessionState({
|
|||||||
if (allMessagesLoadedRef.current) return false;
|
if (allMessagesLoadedRef.current) return false;
|
||||||
if (!hasMoreMessages || !selectedSession || !selectedProject) return false;
|
if (!hasMoreMessages || !selectedSession || !selectedProject) return false;
|
||||||
|
|
||||||
const sessionProvider = selectedSession.__provider || 'claude';
|
|
||||||
|
|
||||||
isLoadingMoreRef.current = true;
|
isLoadingMoreRef.current = true;
|
||||||
const previousScrollHeight = container.scrollHeight;
|
const previousScrollHeight = container.scrollHeight;
|
||||||
const previousScrollTop = container.scrollTop;
|
const previousScrollTop = container.scrollTop;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slot = await sessionStore.fetchMore(selectedSession.id, {
|
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,
|
limit: MESSAGES_PER_PAGE,
|
||||||
});
|
});
|
||||||
if (!slot || slot.serverMessages.length === 0) return false;
|
if (!slot || slot.serverMessages.length === 0) return false;
|
||||||
@@ -458,8 +452,7 @@ export function useChatSessionState({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = (selectedSession.__provider || localStorage.getItem('selected-provider') as Provider) || 'claude';
|
const sessionKey = `${selectedSession.id}:${selectedProject.projectId}`;
|
||||||
const sessionKey = `${selectedSession.id}:${selectedProject.projectId}:${provider}`;
|
|
||||||
|
|
||||||
// Skip if already loaded and fresh
|
// Skip if already loaded and fresh
|
||||||
if (lastLoadedSessionKeyRef.current === sessionKey && sessionStore.has(selectedSession.id) && !sessionStore.isStale(selectedSession.id)) {
|
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
|
// Fetch from server → store updates → chatMessages re-derives automatically
|
||||||
setIsLoadingSessionMessages(true);
|
setIsLoadingSessionMessages(true);
|
||||||
sessionStore.fetchFromServer(selectedSession.id, {
|
sessionStore.fetchFromServer(selectedSession.id, {
|
||||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
|
||||||
projectId: selectedProject.projectId,
|
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
|
||||||
limit: MESSAGES_PER_PAGE,
|
limit: MESSAGES_PER_PAGE,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}).then(slot => {
|
}).then(slot => {
|
||||||
@@ -544,15 +534,9 @@ export function useChatSessionState({
|
|||||||
|
|
||||||
const reloadExternalMessages = async () => {
|
const reloadExternalMessages = async () => {
|
||||||
try {
|
try {
|
||||||
const provider = (localStorage.getItem('selected-provider') as Provider) || 'claude';
|
|
||||||
|
|
||||||
// Skip store refresh during active streaming
|
// Skip store refresh during active streaming
|
||||||
if (!isProcessing) {
|
if (!isProcessing) {
|
||||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
await sessionStore.refreshFromServer(selectedSession.id);
|
||||||
provider: (selectedSession.__provider || provider) as LLMProvider,
|
|
||||||
projectId: selectedProject.projectId,
|
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Boolean(autoScrollToBottom) && isNearBottom()) {
|
if (Boolean(autoScrollToBottom) && isNearBottom()) {
|
||||||
setTimeout(() => scrollToBottom(), 200);
|
setTimeout(() => scrollToBottom(), 200);
|
||||||
@@ -598,13 +582,9 @@ export function useChatSessionState({
|
|||||||
|
|
||||||
const scrollToTarget = async () => {
|
const scrollToTarget = async () => {
|
||||||
if (!allMessagesLoadedRef.current && selectedSession && selectedProject) {
|
if (!allMessagesLoadedRef.current && selectedSession && selectedProject) {
|
||||||
const sessionProvider = selectedSession.__provider || 'claude';
|
|
||||||
try {
|
try {
|
||||||
// Load all messages into the store for search navigation
|
// Load all messages into the store for search navigation
|
||||||
const slot = await sessionStore.fetchFromServer(selectedSession.id, {
|
const slot = await sessionStore.fetchFromServer(selectedSession.id, {
|
||||||
provider: sessionProvider as LLMProvider,
|
|
||||||
projectId: selectedProject.projectId,
|
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
|
||||||
limit: null,
|
limit: null,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
});
|
});
|
||||||
@@ -678,13 +658,10 @@ export function useChatSessionState({
|
|||||||
setTokenBudget(null);
|
setTokenBudget(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sessionProvider = selectedSession.__provider || 'claude';
|
|
||||||
|
|
||||||
const fetchInitialTokenUsage = async () => {
|
const fetchInitialTokenUsage = async () => {
|
||||||
try {
|
try {
|
||||||
// Token usage endpoint is now keyed by the DB projectId.
|
// The backend resolves the provider from the indexed session row.
|
||||||
const params = new URLSearchParams({ provider: sessionProvider });
|
const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage`;
|
||||||
const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage?${params.toString()}`;
|
|
||||||
const response = await authenticatedFetch(url);
|
const response = await authenticatedFetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setTokenBudget(await response.json());
|
setTokenBudget(await response.json());
|
||||||
@@ -696,7 +673,7 @@ export function useChatSessionState({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchInitialTokenUsage();
|
fetchInitialTokenUsage();
|
||||||
}, [selectedProject, selectedSession?.id, selectedSession?.__provider]);
|
}, [selectedProject, selectedSession?.id]);
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
||||||
@@ -756,8 +733,6 @@ export function useChatSessionState({
|
|||||||
const loadAllMessages = useCallback(async () => {
|
const loadAllMessages = useCallback(async () => {
|
||||||
if (!selectedSession || !selectedProject) return;
|
if (!selectedSession || !selectedProject) return;
|
||||||
if (isLoadingAllMessages) return;
|
if (isLoadingAllMessages) return;
|
||||||
const sessionProvider = selectedSession.__provider || 'claude';
|
|
||||||
|
|
||||||
const requestSessionId = selectedSession.id;
|
const requestSessionId = selectedSession.id;
|
||||||
allMessagesLoadedRef.current = true;
|
allMessagesLoadedRef.current = true;
|
||||||
isLoadingMoreRef.current = true;
|
isLoadingMoreRef.current = true;
|
||||||
@@ -770,9 +745,6 @@ export function useChatSessionState({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const slot = await sessionStore.fetchFromServer(requestSessionId, {
|
const slot = await sessionStore.fetchFromServer(requestSessionId, {
|
||||||
provider: sessionProvider as LLMProvider,
|
|
||||||
projectId: selectedProject.projectId,
|
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
|
||||||
limit: null,
|
limit: null,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useWebSocket } from '../../../contexts/WebSocketContext';
|
|||||||
import PermissionContext from '../../../contexts/PermissionContext';
|
import PermissionContext from '../../../contexts/PermissionContext';
|
||||||
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
||||||
import type { ChatInterfaceProps, Provider } from '../types/types';
|
import type { ChatInterfaceProps, Provider } from '../types/types';
|
||||||
import type { LLMProvider } from '../../../types/app';
|
|
||||||
import { useChatProviderState } from '../hooks/useChatProviderState';
|
import { useChatProviderState } from '../hooks/useChatProviderState';
|
||||||
import { useChatSessionState } from '../hooks/useChatSessionState';
|
import { useChatSessionState } from '../hooks/useChatSessionState';
|
||||||
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
||||||
@@ -223,16 +222,7 @@ function ChatInterface({
|
|||||||
// missed live events, and re-attaches a still-running stream to this socket.
|
// missed live events, and re-attaches a still-running stream to this socket.
|
||||||
const handleWebSocketReconnect = useCallback(async () => {
|
const handleWebSocketReconnect = useCallback(async () => {
|
||||||
if (!selectedProject || !selectedSession) return;
|
if (!selectedProject || !selectedSession) return;
|
||||||
const providerVal =
|
await sessionStore.refreshFromServer(selectedSession.id);
|
||||||
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 || '',
|
|
||||||
});
|
|
||||||
statusCheckSentAtRef.current.set(selectedSession.id, Date.now());
|
statusCheckSentAtRef.current.set(selectedSession.id, Date.now());
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: 'chat.subscribe',
|
type: 'chat.subscribe',
|
||||||
|
|||||||
@@ -454,9 +454,6 @@ export function useSessionStore() {
|
|||||||
const fetchFromServer = useCallback(async (
|
const fetchFromServer = useCallback(async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
opts: {
|
opts: {
|
||||||
provider?: LLMProvider;
|
|
||||||
projectId?: string;
|
|
||||||
projectPath?: string;
|
|
||||||
limit?: number | null;
|
limit?: number | null;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
} = {},
|
} = {},
|
||||||
@@ -511,9 +508,6 @@ export function useSessionStore() {
|
|||||||
const fetchMore = useCallback(async (
|
const fetchMore = useCallback(async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
opts: {
|
opts: {
|
||||||
provider?: LLMProvider;
|
|
||||||
projectId?: string;
|
|
||||||
projectPath?: string;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
@@ -592,11 +586,6 @@ export function useSessionStore() {
|
|||||||
*/
|
*/
|
||||||
const refreshFromServer = useCallback(async (
|
const refreshFromServer = useCallback(async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
_opts: {
|
|
||||||
provider?: LLMProvider;
|
|
||||||
projectId?: string;
|
|
||||||
projectPath?: string;
|
|
||||||
} = {},
|
|
||||||
) => {
|
) => {
|
||||||
const slot = getSlot(sessionId);
|
const slot = getSlot(sessionId);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user