Skip to content

Commit 7a2be5c

Browse files
authored
Merge pull request #3289 from bobleer/bob/fix-draft-session-status
fix(web-ui): avoid unavailable status on new session drafts
2 parents a450757 + 852919b commit 7a2be5c

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

‎src/web-ui/src/flow_chat/services/sessionNavStatusService.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,41 @@ const install = (...ids: string[]) => {
114114
};
115115

116116
describe('navigation status synchronization', () => {
117+
it.each(['editing', 'creating'] as const)(
118+
'does not query persisted activity for an unmaterialized %s draft', async phase => {
119+
const id = `draft-${phase}`;
120+
sources.sessions.set(id, { ...row(id), historyState: 'new', isHistorical: false,
121+
draft: { phase, workspaceId: 'workspace-1', turnId: 'first-turn' } });
122+
sources.read.mockResolvedValue(response([]));
123+
install(id);
124+
await vi.advanceTimersByTimeAsync(60_000);
125+
window.dispatchEvent(new Event('focus'));
126+
await vi.advanceTimersByTimeAsync(100);
127+
expect(sources.read).not.toHaveBeenCalled();
128+
expect(sessionNavStatusService.getSnapshot(id).kind).toBe('idle');
129+
},
130+
);
131+
132+
it('starts activity synchronization as soon as a draft is materialized in its chosen workspace', async () => {
133+
const id = 'materialized-draft';
134+
const draft: Session = { ...row(id), historyState: 'new', isHistorical: false,
135+
draft: { phase: 'creating', workspaceId: 'chosen-workspace', turnId: 'first-turn' } };
136+
sources.sessions.set(id, draft);
137+
sources.read.mockResolvedValue(response([activity(id, 'idle')]));
138+
install(id);
139+
await vi.advanceTimersByTimeAsync(100);
140+
sources.sessions.set(id, { ...draft, workspaceId: 'chosen-workspace',
141+
projectWorkspaceId: 'chosen-project', draft: { ...draft.draft!, phase: 'ready' } });
142+
sources.storeListeners.forEach(listener => listener({ sessions: sources.sessions }));
143+
await vi.advanceTimersByTimeAsync(100);
144+
expect(sources.read).toHaveBeenCalledExactlyOnceWith({
145+
sessionId: id, workspaceId: 'chosen-project', limit: 1, sessionIds: [id],
146+
});
147+
expect(sessionNavStatusService.getSnapshot(id).kind).toBe('idle');
148+
sources.events.get('onDialogTurnStarted')!({ sessionId: id, turnId: 'first-turn' });
149+
expect(sessionNavStatusService.getSnapshot(id).kind).toBe('running');
150+
});
151+
117152
it('keeps a local submission running across a host read that predates host acceptance', async () => {
118153
const id = 'local-start-race';
119154
const pending = { id: 'new-turn', status: 'pending', modelRounds: [] } as unknown as Session['dialogTurns'][number];

‎src/web-ui/src/flow_chat/services/sessionNavStatusService.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SessionExecutionState } from '../state-machine/types';
1111
import { driverForSession, sessionDriverNavigationStatusSources } from '../session-drivers/registry';
1212
import { deriveSessionNavStatus, type SessionNavStatus } from '../utils/sessionNavStatus';
1313
import { SessionNavOrdering } from '../utils/sessionNavOrdering';
14+
import { isUnmaterializedSessionDraft } from '../utils/sessionDraft';
1415
import type { Session } from '../types/flow-chat';
1516
import { ensureActivePermissionMailbox, liveSessionInteractionStore } from './liveSessionInteractionStore';
1617
import { SessionActivitySync, type ActivityTarget } from './sessionActivitySync';
@@ -80,7 +81,9 @@ function publish(sessionId: string): void {
8081

8182
function targetFor(sessionId: string): ActivityTarget | undefined {
8283
const session = flowChatStore.getState().sessions.get(sessionId);
83-
if (!session || session.isTransient || session.config.dispatchJobId) return;
84+
// Draft IDs are reserved by the view before the host has a persisted Session.
85+
// An absent activity during editing/creation is expected, not a failed read.
86+
if (!session || session.isTransient || session.config.dispatchJobId || isUnmaterializedSessionDraft(session)) return;
8487
const workspaceId = session.projectWorkspaceId || session.workspaceId || session.config.workspaceId;
8588
if (!workspaceId) return;
8689
return { sessionId, workspaceId };
@@ -205,7 +208,9 @@ export function installSessionNavStatusService(): () => void {
205208
if (prior?.needsUserAttention !== session?.needsUserAttention) {
206209
sessionActivityStore.invalidate(sessionId);
207210
}
208-
if (!prior && session) refresh(sessionId);
211+
if (!prior || (isUnmaterializedSessionDraft(prior) && !isUnmaterializedSessionDraft(session))) {
212+
refresh(sessionId);
213+
}
209214
}
210215
}));
211216
const machineTurns = new Map<string, string>();

0 commit comments

Comments
 (0)