batch3 T5 review fixes: backoff off-by-one + activeSession shape + headers

- useUserEvents: double delay before scheduling, producing 1/2/4/8/16/30s
- useSidebar: activeSessionProjectId -> activeSession {session_id,project_id}
  so consumers can verify URL match and ignore stale values
- api.panes.create/update: drop redundant Content-Type (request helper sets)
- useUserEvents: minimal type guard on incoming WS frame before emit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:28:11 +00:00
parent 8f0e1245d8
commit e82e5670ee
3 changed files with 13 additions and 17 deletions

View File

@@ -123,13 +123,11 @@ export const api = {
create: (sessionId: string, body: PaneCreateRequest) => create: (sessionId: string, body: PaneCreateRequest) =>
request<Pane>(`/api/sessions/${sessionId}/panes`, { request<Pane>(`/api/sessions/${sessionId}/panes`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}), }),
update: (id: string, body: PaneUpdateRequest) => update: (id: string, body: PaneUpdateRequest) =>
request<Pane>(`/api/panes/${id}`, { request<Pane>(`/api/panes/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}), }),
remove: (id: string) => remove: (id: string) =>

View File

@@ -13,7 +13,7 @@ let sharedError: string | null = null;
let sharedLoading: boolean = true; let sharedLoading: boolean = true;
let initialized = false; let initialized = false;
let fetchInFlight: Promise<void> | null = null; let fetchInFlight: Promise<void> | null = null;
let activeSessionProjectId: string | null = null; let activeSession: { session_id: string; project_id: string } | null = null;
const subscribers = new Set<() => void>(); const subscribers = new Set<() => void>();
function notify(): void { function notify(): void {
@@ -150,7 +150,7 @@ sessionEvents.subscribe((event) => {
// session_loaded updates activeSessionProjectId regardless of whether // session_loaded updates activeSessionProjectId regardless of whether
// sharedData is populated yet — notify so subscribers can re-read. // sharedData is populated yet — notify so subscribers can re-read.
if (event.type === 'session_loaded') { if (event.type === 'session_loaded') {
activeSessionProjectId = event.project_id; activeSession = { session_id: event.session_id, project_id: event.project_id };
notify(); notify();
return; return;
} }
@@ -165,11 +165,11 @@ interface Snapshot {
data: SidebarResponse | null; data: SidebarResponse | null;
error: string | null; error: string | null;
loading: boolean; loading: boolean;
activeSessionProjectId: string | null; activeSession: { session_id: string; project_id: string } | null;
} }
function snapshot(): Snapshot { function snapshot(): Snapshot {
return { data: sharedData, error: sharedError, loading: sharedLoading, activeSessionProjectId }; return { data: sharedData, error: sharedError, loading: sharedLoading, activeSession };
} }
export function useSidebar(): { export function useSidebar(): {
@@ -177,7 +177,7 @@ export function useSidebar(): {
error: string | null; error: string | null;
loading: boolean; loading: boolean;
retry: () => void; retry: () => void;
activeSessionProjectId: string | null; activeSession: { session_id: string; project_id: string } | null;
} { } {
const [state, setState] = useState<Snapshot>(snapshot); const [state, setState] = useState<Snapshot>(snapshot);
@@ -199,5 +199,5 @@ export function useSidebar(): {
void load(); void load();
}; };
return { data: state.data, error: state.error, loading: state.loading, retry, activeSessionProjectId: state.activeSessionProjectId }; return { data: state.data, error: state.error, loading: state.loading, retry, activeSession: state.activeSession };
} }

View File

@@ -23,11 +23,10 @@ export function useUserEvents(): void {
ws.onmessage = (ev) => { ws.onmessage = (ev) => {
try { try {
const frame = JSON.parse(ev.data); const parsed: unknown = JSON.parse(ev.data);
// The server emits frames whose `type` matches SessionEvent union members if (parsed && typeof (parsed as { type?: unknown }).type === 'string') {
// (project_created, project_deleted, session_created, session_deleted, session_updated). sessionEvents.emit(parsed as import('./sessionEvents').SessionEvent);
// Pass through onto the bus. }
sessionEvents.emit(frame);
} catch (err) { } catch (err) {
console.warn('useUserEvents: failed to parse frame', err); console.warn('useUserEvents: failed to parse frame', err);
} }
@@ -35,10 +34,9 @@ export function useUserEvents(): void {
ws.onclose = () => { ws.onclose = () => {
if (unmounted) return; if (unmounted) return;
reconnectTimer = setTimeout(() => { const delay = reconnectDelay;
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS); reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
connect(); reconnectTimer = setTimeout(connect, delay);
}, reconnectDelay);
}; };
ws.onerror = () => { ws.onerror = () => {