feat(vibe): reconcile mutable session previews in playback

This commit is contained in:
kami
2026-08-01 23:47:02 +04:00
parent 51ef7c84db
commit 57df1cfe9f
20 changed files with 1311 additions and 312 deletions
+24 -26
View File
@@ -1,30 +1,34 @@
import { create } from 'zustand';
import type { Track, VibeSession } from '../types';
import type { VibePlanItem } from '../services/vibeService';
// V2 recommendation session state. The backend stores the plan in Redis
// (keyed by sessionId) and serves tracks one at a time via GET /v2/vibe/next.
// We keep a lookahead buffer of upcoming Track[] to feed playback.
// The durable plan is authoritative. `buffer` is only its currently
// uncommitted, hydrated preview; it may be replaced at any feedback boundary.
interface VibeState {
activeSessionId: string | null;
seedTrackId: string | null;
planVersion: number | null;
/** Durable cursor for the track currently in Vibe playback. */
currentPlanItem: VibePlanItem | null;
centerTrack: Track | null;
buffer: Track[]; // lookahead buffer of upcoming recommended tracks
/** Outcome of the first V2 batch, including sessions initiated from Discover. */
buffer: Track[];
initialBatchStatus: 'idle' | 'loading' | 'exhausted' | 'failed';
setActiveSession: (session: VibeSession | null) => void;
setSeedTrackId: (seedTrackId: string | null) => void;
setCenterTrack: (track: Track | null) => void;
setBuffer: (buffer: Track[]) => void;
/** Returns false when a response belongs to an older plan revision. */
setPlan: (planVersion: number | null, preview: Track[]) => boolean;
setCurrentPlanItem: (item: VibePlanItem | null) => void;
setInitialBatchStatus: (status: VibeState['initialBatchStatus']) => void;
appendBuffer: (tracks: Track[]) => void;
shiftBuffer: () => Track | undefined;
reset: () => void;
}
const initialState = {
activeSessionId: null as string | null,
seedTrackId: null as string | null,
planVersion: null as number | null,
currentPlanItem: null as VibePlanItem | null,
centerTrack: null as Track | null,
buffer: [] as Track[],
initialBatchStatus: 'idle' as const,
@@ -33,26 +37,20 @@ const initialState = {
export const useVibeStore = create<VibeState>((set, get) => ({
...initialState,
setActiveSession: (session) =>
set(
session
? { activeSessionId: session.sessionId, seedTrackId: session.seedTrackId }
: { activeSessionId: null, seedTrackId: null }
),
setActiveSession: (session) => set(
session
? { activeSessionId: session.sessionId, seedTrackId: session.seedTrackId }
: { activeSessionId: null, seedTrackId: null },
),
setSeedTrackId: (seedTrackId) => set({ seedTrackId }),
setCenterTrack: (centerTrack) => set({ centerTrack }),
setBuffer: (buffer) => set({ buffer }),
setInitialBatchStatus: (initialBatchStatus) => set({ initialBatchStatus }),
appendBuffer: (tracks) => set((state) => ({ buffer: [...state.buffer, ...tracks] })),
shiftBuffer: () => {
const { buffer } = get();
if (buffer.length === 0) return undefined;
const [head, ...rest] = buffer;
set({ buffer: rest });
return head;
setPlan: (planVersion, buffer) => {
const current = get().planVersion;
if (planVersion === null || (current !== null && planVersion < current)) return false;
set({ planVersion, buffer });
return true;
},
setCurrentPlanItem: (currentPlanItem) => set({ currentPlanItem }),
setInitialBatchStatus: (initialBatchStatus) => set({ initialBatchStatus }),
reset: () => set({ ...initialState }),
}));