batch3 T5: frontend foundation — Pane types, panes API, user-events WS
- Mirror Pane/PaneState/UserStream types - api.panes.* CRUD methods - sessionEvents adds session_updated, session_loaded, open_file_in_browser - useUserEvents hook: single app-level WS to /api/ws/user with reconnect - useSidebar handles session_updated (in-place patch + re-sort) and session_loaded (active-project highlight gap fix); open_file_in_browser is a deliberate no-op here, consumed by Workspace later - App.tsx mounts useUserEvents once Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ let sharedError: string | null = null;
|
||||
let sharedLoading: boolean = true;
|
||||
let initialized = false;
|
||||
let fetchInFlight: Promise<void> | null = null;
|
||||
let activeSessionProjectId: string | null = null;
|
||||
const subscribers = new Set<() => void>();
|
||||
|
||||
function notify(): void {
|
||||
@@ -74,6 +75,7 @@ function applyEvent(prev: SidebarResponse, event: import('./sessionEvents').Sess
|
||||
name: event.session.name,
|
||||
model: event.session.model,
|
||||
updated_at: event.session.updated_at,
|
||||
project_id: event.project_id,
|
||||
};
|
||||
return {
|
||||
...p,
|
||||
@@ -113,7 +115,30 @@ function applyEvent(prev: SidebarResponse, event: import('./sessionEvents').Sess
|
||||
});
|
||||
return changed ? { ...prev, projects } : prev;
|
||||
}
|
||||
default:
|
||||
case 'session_updated': {
|
||||
let changed = false;
|
||||
const projects = prev.projects.map((p) => {
|
||||
if (p.id !== event.project_id) return p;
|
||||
let projectChanged = false;
|
||||
const recent = p.recent_sessions.map((s) => {
|
||||
if (s.id !== event.session_id) return s;
|
||||
projectChanged = true;
|
||||
return { ...s, name: event.name, updated_at: event.updated_at };
|
||||
});
|
||||
if (!projectChanged) return p;
|
||||
changed = true;
|
||||
const sorted = [...recent].sort(
|
||||
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
);
|
||||
return { ...p, recent_sessions: sorted };
|
||||
});
|
||||
return changed ? { ...prev, projects } : prev;
|
||||
}
|
||||
case 'session_loaded':
|
||||
// activeSessionProjectId is updated in the subscribe callback; no data change here.
|
||||
return prev;
|
||||
case 'open_file_in_browser':
|
||||
// Consumed by Workspace (T7); no sidebar state change needed.
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
@@ -122,6 +147,13 @@ function applyEvent(prev: SidebarResponse, event: import('./sessionEvents').Sess
|
||||
// before the initial fetch resolves are dropped; the eventual fetch
|
||||
// result is the source of truth.
|
||||
sessionEvents.subscribe((event) => {
|
||||
// session_loaded updates activeSessionProjectId regardless of whether
|
||||
// sharedData is populated yet — notify so subscribers can re-read.
|
||||
if (event.type === 'session_loaded') {
|
||||
activeSessionProjectId = event.project_id;
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
if (!sharedData) return;
|
||||
const next = applyEvent(sharedData, event);
|
||||
if (next === sharedData) return;
|
||||
@@ -133,10 +165,11 @@ interface Snapshot {
|
||||
data: SidebarResponse | null;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
activeSessionProjectId: string | null;
|
||||
}
|
||||
|
||||
function snapshot(): Snapshot {
|
||||
return { data: sharedData, error: sharedError, loading: sharedLoading };
|
||||
return { data: sharedData, error: sharedError, loading: sharedLoading, activeSessionProjectId };
|
||||
}
|
||||
|
||||
export function useSidebar(): {
|
||||
@@ -144,6 +177,7 @@ export function useSidebar(): {
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
activeSessionProjectId: string | null;
|
||||
} {
|
||||
const [state, setState] = useState<Snapshot>(snapshot);
|
||||
|
||||
@@ -165,5 +199,5 @@ export function useSidebar(): {
|
||||
void load();
|
||||
};
|
||||
|
||||
return { data: state.data, error: state.error, loading: state.loading, retry };
|
||||
return { data: state.data, error: state.error, loading: state.loading, retry, activeSessionProjectId: state.activeSessionProjectId };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user