Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,11 @@ export async function switchChatSession(
if (shouldHydrateBeforeSwitch) {
try {
await hydrateHistoricalSession(context, sessionId, true, {
// Programmatic opens (including pet bubbles) hydrate before selection.
// An active-only hydrate would discard their restored records as stale
// and then activate a metadata-only session with no load left running.
// Also upgrades any speculative active-only preload we are reusing.
deferFullHistoryUntilActive: shouldActivateBeforeHydrate,
isRetryStillRelevant: () => (
surfaceScope.isCurrent() && switchRequestId === latestSwitchRequestId && isStillRelevant()
),
Expand Down
59 changes: 59 additions & 0 deletions src/web-ui/src/flow_chat/store/FlowChatStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import {
askUserQuestionDraftStore,
} from './askUserQuestionDraftStore';

vi.mock('@/shared/notification-system', () => ({
notificationService: { error: vi.fn(), warning: vi.fn() },
}));

vi.mock('../session-drivers/registry', () => ({
driverForSession: () => ({ id: 'local' }),
}));

const workspaceFixtures = vi.hoisted(() => new Map<string, any>());
vi.mock('@/infrastructure/services/business/workspaceManager', () => ({
workspaceManager: { getState: () => ({ openedWorkspaces: workspaceFixtures, recentWorkspaces: [] }) },
Expand Down Expand Up @@ -4343,6 +4351,57 @@ describe('FlowChatStore historical session hydration state', () => {
}
});

it.each([false, true])('opens an inactive historical session without a sidebar pointer intent (preload: %s)', async (preload) => {
vi.useFakeTimers();
try {
const { switchChatSession, preloadHistoricalSessionForOpen, pendingHistoryLoadKey } =
await import('../services/flow-chat-manager/SessionModule');
const session = createSession({
sessionId: 'history-1', isHistorical: true, historyState: 'metadata-only',
});
flowChatStore.setState(() => ({
sessions: new Map([[session.sessionId, session]]),
activeSessionId: null,
}));
apiMocks.restoreSessionView.mockResolvedValue({
session: {
sessionId: session.sessionId, sessionName: 'Saved session',
agentType: 'Standard', state: 'Idle', turnCount: 1, createdAt: 1,
},
turns: [{
turnId: 'saved-turn', turnIndex: 0, sessionId: session.sessionId,
timestamp: 1, startTime: 1, status: 'completed', modelRounds: [],
userMessage: { id: 'saved-message', content: 'Saved prompt', timestamp: 1 },
}],
contextRestoreState: 'ready', isPartial: false,
loadedTurnCount: 1, totalTurnCount: 1,
});
// Exercise the real manager/store boundary used by pet and other
// programmatic openers; mocking loadSessionHistory hid this regression.
const context = {
flowChatStore, pendingHistoryLoads: new Map(),
} as unknown as import('../services/flow-chat-manager/types').FlowChatContext;
if (preload) {
const competingKey = pendingHistoryLoadKey('other-session');
context.pendingHistoryLoads.set(competingKey, Promise.resolve());
preloadHistoricalSessionForOpen(context, session.sessionId);
context.pendingHistoryLoads.delete(competingKey);
}
await switchChatSession(context, session.sessionId);

expect(flowChatStore.getState().activeSessionId).toBe(session.sessionId);
expect(flowChatStore.getState().sessions.get(session.sessionId)).toMatchObject({
historyState: 'ready',
dialogTurns: [expect.objectContaining({ id: 'saved-turn' })],
});
expect(context.pendingHistoryLoads.size).toBe(0);
expect(apiMocks.restoreSessionView).toHaveBeenCalledTimes(preload ? 2 : 1);
} finally {
vi.clearAllTimers();
vi.useRealTimers();
}
});

it('skips committing stale local history hydrate when switching away before restore finishes', async () => {
vi.useFakeTimers();
const latestTurn = {
Expand Down
Loading