mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-01 09:55:34 +08:00
fix: stabilize opencode session startup
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
export { initializeDatabase } from '@/modules/database/init-db.js';
|
export { initializeDatabase } from '@/modules/database/init-db.js';
|
||||||
|
export { closeConnection, getConnection, getDatabasePath } from '@/modules/database/connection.js';
|
||||||
export { apiKeysDb } from '@/modules/database/repositories/api-keys.js';
|
export { apiKeysDb } from '@/modules/database/repositories/api-keys.js';
|
||||||
export { appConfigDb } from '@/modules/database/repositories/app-config.js';
|
export { appConfigDb } from '@/modules/database/repositories/app-config.js';
|
||||||
export { credentialsDb } from '@/modules/database/repositories/credentials.js';
|
export { credentialsDb } from '@/modules/database/repositories/credentials.js';
|
||||||
|
|||||||
@@ -58,15 +58,46 @@ const formatToolContent = (value: unknown): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractText = (value: unknown): string => {
|
/**
|
||||||
if (typeof value === 'string') {
|
* OpenCode can persist the first prompt as a JSON string literal inside a text
|
||||||
|
* part, for example `"hello"` instead of `hello`. Decode only complete JSON
|
||||||
|
* string literals so normal assistant/user prose remains untouched.
|
||||||
|
*/
|
||||||
|
const unwrapJsonStringLiteral = (value: string): string => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
return typeof parsed === 'string' ? parsed : value;
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractText = (value: unknown): string => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return unwrapJsonStringLiteral(value);
|
||||||
|
}
|
||||||
|
|
||||||
const record = readObjectRecord(value);
|
const record = readObjectRecord(value);
|
||||||
return readOptionalString(record?.text)
|
const text = readOptionalString(record?.text)
|
||||||
?? readOptionalString(record?.content)
|
?? readOptionalString(record?.content)
|
||||||
?? '';
|
?? '';
|
||||||
|
return unwrapJsonStringLiteral(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUserRole = (value: unknown): boolean => {
|
||||||
|
const record = readObjectRecord(value);
|
||||||
|
return readOptionalString(record?.role) === 'user';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserTextEcho = (raw: AnyRecord): boolean => {
|
||||||
|
return readOptionalString(raw.role) === 'user'
|
||||||
|
|| hasUserRole(raw.message)
|
||||||
|
|| hasUserRole(raw.part);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildTokenUsage = (totals: OpenCodeTokenTotals | undefined): AnyRecord | undefined => {
|
const buildTokenUsage = (totals: OpenCodeTokenTotals | undefined): AnyRecord | undefined => {
|
||||||
@@ -158,6 +189,12 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
|
|||||||
?? generateMessageId('opencode');
|
?? generateMessageId('opencode');
|
||||||
|
|
||||||
if (type === 'text') {
|
if (type === 'text') {
|
||||||
|
// The client already renders an optimistic user bubble, so provider user
|
||||||
|
// echoes must not be streamed back as assistant text.
|
||||||
|
if (isUserTextEcho(raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const content = extractText(raw.text ?? raw.delta ?? raw.message);
|
const content = extractText(raw.text ?? raw.delta ?? raw.message);
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import test from 'node:test';
|
|||||||
|
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
|
|
||||||
import { closeConnection } from '@/modules/database/connection.js';
|
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
||||||
import { initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
||||||
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
||||||
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
||||||
|
|
||||||
@@ -183,7 +182,7 @@ const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): P
|
|||||||
1_700_000_001_000,
|
1_700_000_001_000,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Build the OpenCode integration.',
|
text: JSON.stringify('Build the OpenCode integration.'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
insertPart.run(
|
insertPart.run(
|
||||||
@@ -261,6 +260,28 @@ test('OpenCode session synchronizer indexes sqlite sessions without deletable tr
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => {
|
||||||
|
const provider = new OpenCodeSessionsProvider();
|
||||||
|
const normalized = provider.normalizeMessage({
|
||||||
|
type: 'text',
|
||||||
|
sessionID: 'open-session-live',
|
||||||
|
text: JSON.stringify('hello bro'),
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
assert.equal(normalized.length, 1);
|
||||||
|
assert.equal(normalized[0]?.kind, 'stream_delta');
|
||||||
|
assert.equal(normalized[0]?.content, 'hello bro');
|
||||||
|
|
||||||
|
const userEcho = provider.normalizeMessage({
|
||||||
|
type: 'text',
|
||||||
|
sessionID: 'open-session-live',
|
||||||
|
role: 'user',
|
||||||
|
text: 'hello bro',
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
assert.deepEqual(userEcho, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('OpenCode sessions provider reads sqlite history and token usage', { concurrency: false }, async () => {
|
test('OpenCode sessions provider reads sqlite history and token usage', { concurrency: false }, async () => {
|
||||||
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-history-'));
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-history-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
@@ -275,6 +296,7 @@ test('OpenCode sessions provider reads sqlite history and token usage', { concur
|
|||||||
assert.equal(history.total, 4);
|
assert.equal(history.total, 4);
|
||||||
assert.equal(history.messages[0]?.kind, 'text');
|
assert.equal(history.messages[0]?.kind, 'text');
|
||||||
assert.equal(history.messages[0]?.role, 'user');
|
assert.equal(history.messages[0]?.role, 'user');
|
||||||
|
assert.equal(history.messages[0]?.content, 'Build the OpenCode integration.');
|
||||||
assert.equal(history.messages[1]?.kind, 'thinking');
|
assert.equal(history.messages[1]?.kind, 'thinking');
|
||||||
assert.equal(history.messages[2]?.content, 'The provider is wired.');
|
assert.equal(history.messages[2]?.content, 'The provider is wired.');
|
||||||
assert.equal(history.messages[3]?.kind, 'tool_use');
|
assert.equal(history.messages[3]?.kind, 'tool_use');
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { useFileMentions } from './useFileMentions';
|
|||||||
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
|
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
|
||||||
|
|
||||||
type PendingViewSession = {
|
type PendingViewSession = {
|
||||||
sessionId: string | null;
|
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -566,13 +565,9 @@ export function useChatComposerState({
|
|||||||
setTimeout(() => scrollToBottom(), 100);
|
setTimeout(() => scrollToBottom(), 100);
|
||||||
|
|
||||||
if (!effectiveSessionId && !selectedSession?.id) {
|
if (!effectiveSessionId && !selectedSession?.id) {
|
||||||
if (typeof window !== 'undefined') {
|
// This tracks only that a request is in flight before the provider has
|
||||||
// Reset stale pending IDs from previous interrupted runs before creating a new one.
|
// emitted its real session id; routing still waits for session_created.
|
||||||
sessionStorage.removeItem('pendingSessionId');
|
pendingViewSessionRef.current = { startedAt: Date.now() };
|
||||||
}
|
|
||||||
// For new sessions we intentionally keep this as `null` until the backend
|
|
||||||
// emits `session_created` with the canonical provider session id.
|
|
||||||
pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() };
|
|
||||||
}
|
}
|
||||||
if (effectiveSessionId) {
|
if (effectiveSessionId) {
|
||||||
onSessionActive?.(effectiveSessionId);
|
onSessionActive?.(effectiveSessionId);
|
||||||
@@ -884,15 +879,11 @@ export function useChatComposerState({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingSessionId =
|
|
||||||
typeof window !== 'undefined' ? sessionStorage.getItem('pendingSessionId') : null;
|
|
||||||
const cursorSessionId =
|
const cursorSessionId =
|
||||||
typeof window !== 'undefined' ? sessionStorage.getItem('cursorSessionId') : null;
|
typeof window !== 'undefined' ? sessionStorage.getItem('cursorSessionId') : null;
|
||||||
|
|
||||||
const candidateSessionIds = [
|
const candidateSessionIds = [
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
pendingViewSessionRef.current?.sessionId || null,
|
|
||||||
pendingSessionId,
|
|
||||||
provider === 'cursor' ? cursorSessionId : null,
|
provider === 'cursor' ? cursorSessionId : null,
|
||||||
selectedSession?.id || null,
|
selectedSession?.id || null,
|
||||||
];
|
];
|
||||||
@@ -910,7 +901,7 @@ export function useChatComposerState({
|
|||||||
sessionId: targetSessionId,
|
sessionId: targetSessionId,
|
||||||
provider,
|
provider,
|
||||||
});
|
});
|
||||||
}, [canAbortSession, currentSessionId, pendingViewSessionRef, provider, selectedSession?.id, sendMessage]);
|
}, [canAbortSession, currentSessionId, provider, selectedSession?.id, sendMessage]);
|
||||||
|
|
||||||
const handleGrantToolPermission = useCallback(
|
const handleGrantToolPermission = useCallback(
|
||||||
(suggestion: { entry: string; toolName: string }) => {
|
(suggestion: { entry: string; toolName: string }) => {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type { ProjectSession, LLMProvider } from '../../../types/app';
|
|||||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||||
|
|
||||||
type PendingViewSession = {
|
type PendingViewSession = {
|
||||||
sessionId: string | null;
|
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,6 +62,7 @@ interface UseChatRealtimeHandlersArgs {
|
|||||||
streamTimerRef: MutableRefObject<number | null>;
|
streamTimerRef: MutableRefObject<number | null>;
|
||||||
accumulatedStreamRef: MutableRefObject<string>;
|
accumulatedStreamRef: MutableRefObject<string>;
|
||||||
onSessionInactive?: (sessionId?: string | null) => void;
|
onSessionInactive?: (sessionId?: string | null) => void;
|
||||||
|
onSessionActive?: (sessionId?: string | null) => void;
|
||||||
onSessionProcessing?: (sessionId?: string | null) => void;
|
onSessionProcessing?: (sessionId?: string | null) => void;
|
||||||
onSessionNotProcessing?: (sessionId?: string | null) => void;
|
onSessionNotProcessing?: (sessionId?: string | null) => void;
|
||||||
onNavigateToSession?: (sessionId: string, options?: SessionNavigationOptions) => void;
|
onNavigateToSession?: (sessionId: string, options?: SessionNavigationOptions) => void;
|
||||||
@@ -89,6 +89,7 @@ export function useChatRealtimeHandlers({
|
|||||||
streamTimerRef,
|
streamTimerRef,
|
||||||
accumulatedStreamRef,
|
accumulatedStreamRef,
|
||||||
onSessionInactive,
|
onSessionInactive,
|
||||||
|
onSessionActive,
|
||||||
onSessionProcessing,
|
onSessionProcessing,
|
||||||
onSessionNotProcessing,
|
onSessionNotProcessing,
|
||||||
onNavigateToSession,
|
onNavigateToSession,
|
||||||
@@ -104,7 +105,7 @@ export function useChatRealtimeHandlers({
|
|||||||
lastProcessedMessageRef.current = latestMessage;
|
lastProcessedMessageRef.current = latestMessage;
|
||||||
|
|
||||||
const activeViewSessionId =
|
const activeViewSessionId =
|
||||||
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
selectedSession?.id || currentSessionId || null;
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- */
|
/* ---------------------------------------------------------------- */
|
||||||
/* Legacy messages (no `kind` field) — handle and return */
|
/* Legacy messages (no `kind` field) — handle and return */
|
||||||
@@ -151,10 +152,12 @@ export function useChatRealtimeHandlers({
|
|||||||
statusSessionId === currentSessionId || (selectedSession && statusSessionId === selectedSession.id);
|
statusSessionId === currentSessionId || (selectedSession && statusSessionId === selectedSession.id);
|
||||||
|
|
||||||
if (msg.isProcessing) {
|
if (msg.isProcessing) {
|
||||||
|
onSessionActive?.(statusSessionId);
|
||||||
onSessionProcessing?.(statusSessionId);
|
onSessionProcessing?.(statusSessionId);
|
||||||
if (isCurrentSession) { setIsLoading(true); setCanAbortSession(true); }
|
if (isCurrentSession) { setIsLoading(true); setCanAbortSession(true); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSessionInactive?.(statusSessionId);
|
onSessionInactive?.(statusSessionId);
|
||||||
onSessionNotProcessing?.(statusSessionId);
|
onSessionNotProcessing?.(statusSessionId);
|
||||||
if (isCurrentSession) {
|
if (isCurrentSession) {
|
||||||
@@ -235,15 +238,21 @@ export function useChatRealtimeHandlers({
|
|||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
console.log('Session created with ID:', newSessionId);
|
console.log('Session created with ID:', newSessionId);
|
||||||
console.log('Existing session ID:', currentSessionId);
|
console.log('Existing session ID:', currentSessionId);
|
||||||
sessionStorage.setItem('pendingSessionId', newSessionId);
|
|
||||||
if (pendingViewSessionRef.current && !pendingViewSessionRef.current.sessionId) {
|
|
||||||
pendingViewSessionRef.current.sessionId = newSessionId;
|
|
||||||
}
|
|
||||||
setCurrentSessionId(newSessionId);
|
setCurrentSessionId(newSessionId);
|
||||||
setPendingPermissionRequests((prev) =>
|
setPendingPermissionRequests((prev) =>
|
||||||
prev.map((r) => (r.sessionId ? r : { ...r, sessionId: newSessionId })),
|
prev.map((r) => (r.sessionId ? r : { ...r, sessionId: newSessionId })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
|
onSessionActive?.(newSessionId);
|
||||||
|
onSessionProcessing?.(newSessionId);
|
||||||
|
setIsLoading(true);
|
||||||
|
setCanAbortSession(true);
|
||||||
|
setClaudeStatus({
|
||||||
|
text: 'Processing',
|
||||||
|
tokens: 0,
|
||||||
|
can_interrupt: true,
|
||||||
|
});
|
||||||
onNavigateToSession?.(newSessionId);
|
onNavigateToSession?.(newSessionId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -266,6 +275,7 @@ export function useChatRealtimeHandlers({
|
|||||||
setPendingPermissionRequests([]);
|
setPendingPermissionRequests([]);
|
||||||
onSessionInactive?.(sid);
|
onSessionInactive?.(sid);
|
||||||
onSessionNotProcessing?.(sid);
|
onSessionNotProcessing?.(sid);
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
|
|
||||||
// Handle aborted case
|
// Handle aborted case
|
||||||
if (msg.aborted) {
|
if (msg.aborted) {
|
||||||
@@ -279,16 +289,10 @@ export function useChatRealtimeHandlers({
|
|||||||
typeof msg.actualSessionId === 'string' && msg.actualSessionId.trim().length > 0
|
typeof msg.actualSessionId === 'string' && msg.actualSessionId.trim().length > 0
|
||||||
? msg.actualSessionId
|
? msg.actualSessionId
|
||||||
: null;
|
: null;
|
||||||
const pendingSessionId = sessionStorage.getItem('pendingSessionId');
|
|
||||||
const completedSuccessfully = msg.exitCode === undefined || msg.exitCode === 0;
|
|
||||||
const isVisibleSession =
|
const isVisibleSession =
|
||||||
Boolean(
|
Boolean(
|
||||||
sid
|
sid
|
||||||
&& (
|
&& sid === activeViewSessionId,
|
||||||
sid === activeViewSessionId
|
|
||||||
|| sid === pendingSessionId
|
|
||||||
|| pendingViewSessionRef.current?.sessionId === sid
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (actualSessionId && sid && actualSessionId !== sid) {
|
if (actualSessionId && sid && actualSessionId !== sid) {
|
||||||
@@ -296,17 +300,6 @@ export function useChatRealtimeHandlers({
|
|||||||
|
|
||||||
if (isVisibleSession) {
|
if (isVisibleSession) {
|
||||||
setCurrentSessionId(actualSessionId);
|
setCurrentSessionId(actualSessionId);
|
||||||
|
|
||||||
if (pendingViewSessionRef.current) {
|
|
||||||
const pendingSession = pendingViewSessionRef.current.sessionId;
|
|
||||||
if (!pendingSession || pendingSession === sid) {
|
|
||||||
pendingViewSessionRef.current.sessionId = actualSessionId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completedSuccessfully && pendingSessionId === sid) {
|
|
||||||
sessionStorage.removeItem('pendingSessionId');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVisibleSession) {
|
if (isVisibleSession) {
|
||||||
@@ -316,16 +309,6 @@ export function useChatRealtimeHandlers({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear pending session
|
|
||||||
if (pendingSessionId && !currentSessionId && completedSuccessfully) {
|
|
||||||
const resolvedSessionId = actualSessionId || pendingSessionId;
|
|
||||||
setCurrentSessionId(resolvedSessionId);
|
|
||||||
if (actualSessionId) {
|
|
||||||
onNavigateToSession?.(resolvedSessionId, { replace: true });
|
|
||||||
}
|
|
||||||
sessionStorage.removeItem('pendingSessionId');
|
|
||||||
setTimeout(() => { void paletteOps.refreshProjects(); }, 500);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +318,7 @@ export function useChatRealtimeHandlers({
|
|||||||
setClaudeStatus(null);
|
setClaudeStatus(null);
|
||||||
onSessionInactive?.(sid);
|
onSessionInactive?.(sid);
|
||||||
onSessionNotProcessing?.(sid);
|
onSessionNotProcessing?.(sid);
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +383,7 @@ export function useChatRealtimeHandlers({
|
|||||||
streamTimerRef,
|
streamTimerRef,
|
||||||
accumulatedStreamRef,
|
accumulatedStreamRef,
|
||||||
onSessionInactive,
|
onSessionInactive,
|
||||||
|
onSessionActive,
|
||||||
onSessionProcessing,
|
onSessionProcessing,
|
||||||
onSessionNotProcessing,
|
onSessionNotProcessing,
|
||||||
onNavigateToSession,
|
onNavigateToSession,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const MESSAGES_PER_PAGE = 20;
|
|||||||
const INITIAL_VISIBLE_MESSAGES = 100;
|
const INITIAL_VISIBLE_MESSAGES = 100;
|
||||||
|
|
||||||
type PendingViewSession = {
|
type PendingViewSession = {
|
||||||
sessionId: string | null;
|
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,7 +159,7 @@ export function useChatSessionState({
|
|||||||
* Why this is essential:
|
* Why this is essential:
|
||||||
* - Chat keeps local state that is not fully derived from `selectedSession`:
|
* - Chat keeps local state that is not fully derived from `selectedSession`:
|
||||||
* `currentSessionId`, `pendingUserMessage`, streaming/status flags, message
|
* `currentSessionId`, `pendingUserMessage`, streaming/status flags, message
|
||||||
* pagination/scroll bookkeeping, and pending session IDs in sessionStorage.
|
* pagination/scroll bookkeeping, and provider-specific sessionStorage keys.
|
||||||
* - If the user clicks New Session while already on the same route with no
|
* - 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
|
* selected session, parent state updates can be idempotent and this local
|
||||||
* state would otherwise persist, making the click appear to "do nothing".
|
* state would otherwise persist, making the click appear to "do nothing".
|
||||||
@@ -177,7 +176,6 @@ export function useChatSessionState({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCurrentSessionId(null);
|
setCurrentSessionId(null);
|
||||||
setPendingUserMessage(null);
|
setPendingUserMessage(null);
|
||||||
sessionStorage.removeItem('pendingSessionId');
|
|
||||||
sessionStorage.removeItem('cursorSessionId');
|
sessionStorage.removeItem('cursorSessionId');
|
||||||
messagesOffsetRef.current = 0;
|
messagesOffsetRef.current = 0;
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
@@ -396,6 +394,12 @@ export function useChatSessionState({
|
|||||||
// Main session loading effect — store-based
|
// Main session loading effect — store-based
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSession || !selectedProject) {
|
if (!selectedSession || !selectedProject) {
|
||||||
|
// A new provider run can be in flight before the router has a canonical
|
||||||
|
// selectedSession. Keep the processing banner alive until complete/error.
|
||||||
|
if (pendingViewSessionRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
resetStreamingState();
|
resetStreamingState();
|
||||||
pendingViewSessionRef.current = null;
|
pendingViewSessionRef.current = null;
|
||||||
setClaudeStatus(null);
|
setClaudeStatus(null);
|
||||||
@@ -537,10 +541,6 @@ export function useChatSessionState({
|
|||||||
}
|
}
|
||||||
}, [selectedSession]);
|
}, [selectedSession]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSession?.id) pendingViewSessionRef.current = null;
|
|
||||||
}, [pendingViewSessionRef, selectedSession?.id]);
|
|
||||||
|
|
||||||
// Scroll to search target
|
// Scroll to search target
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchTarget || chatMessages.length === 0 || isLoadingSessionMessages) return;
|
if (!searchTarget || chatMessages.length === 0 || isLoadingSessionMessages) return;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import ChatComposer from './subcomponents/ChatComposer';
|
|||||||
|
|
||||||
|
|
||||||
type PendingViewSession = {
|
type PendingViewSession = {
|
||||||
sessionId: string | null;
|
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +239,7 @@ function ChatInterface({
|
|||||||
streamTimerRef,
|
streamTimerRef,
|
||||||
accumulatedStreamRef,
|
accumulatedStreamRef,
|
||||||
onSessionInactive,
|
onSessionInactive,
|
||||||
|
onSessionActive,
|
||||||
onSessionProcessing,
|
onSessionProcessing,
|
||||||
onSessionNotProcessing,
|
onSessionNotProcessing,
|
||||||
onNavigateToSession,
|
onNavigateToSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user