sidebar turn-state: instant lh:turn-state propagation, poll demoted to backstop
This commit is contained in:
@@ -61,6 +61,24 @@ export function useChatSession({ chatId, initialMessages, initiallyInFlight }: U
|
||||
// The view + state, derived per model change from the model.
|
||||
const state = useMemo<SessionState>(() => model.getState(), [model, version] as const);
|
||||
|
||||
// THE TURN-STATE BROADCAST (owner, sidebar round 2026-10-02): the same
|
||||
// optimistic machine, propagated — the instant a turn starts locally
|
||||
// (submitted: send, answer, session start, reattach, wake attach) the
|
||||
// sidebar learns it without waiting for its 2.5s poll; at settle the
|
||||
// event clears the row and triggers one immediate refetch. The model
|
||||
// stays pure; this adapter owns the window seam (the lh:chat-renamed
|
||||
// pattern).
|
||||
const prevStatusRef = useRef(state.status);
|
||||
useEffect(() => {
|
||||
const was = prevStatusRef.current;
|
||||
prevStatusRef.current = state.status;
|
||||
if (was === state.status) return;
|
||||
const running = state.status === "submitted" || state.status === "streaming";
|
||||
const wasRunning = was === "submitted" || was === "streaming";
|
||||
if (running === wasRunning) return; // submitted→streaming: still running; error↔ready: not
|
||||
window.dispatchEvent(new CustomEvent("lh:turn-state", { detail: { id: chatId, running } }));
|
||||
}, [state.status, chatId]);
|
||||
|
||||
// The reattach settled terminal → the sync-and-continue seam (chat.tsx).
|
||||
const setReattachSettled = useCallback(
|
||||
(fn: () => void) => {
|
||||
|
||||
@@ -373,6 +373,36 @@ function SessionsSidebar() {
|
||||
return () => window.removeEventListener("lh:chat-renamed", onRenamed);
|
||||
}, []);
|
||||
|
||||
/* INSTANT TURN-STATE (owner, sidebar round 2026-10-02): the open
|
||||
session broadcasts lh:turn-state the instant its turn starts
|
||||
(submitted — the optimistic machine's own flip) and at settle —
|
||||
busy mirrors the server's own definition (isTurnRunning), so the
|
||||
patch is exact: spinner up the moment the action happens, cleared
|
||||
and refetched once at settle (title/order move with turns). The
|
||||
2.5s poll stays as the BACKSTOP for everything only the server
|
||||
knows: other windows, wake turns in unopened sessions. A start on
|
||||
a chat not yet in the snapshot (a brand-new session) = one
|
||||
immediate refresh instead of a patch. */
|
||||
const chatsRef = useRef(chats);
|
||||
chatsRef.current = chats;
|
||||
useEffect(() => {
|
||||
const onTurnState = (e: Event) => {
|
||||
const { id, running } = (e as CustomEvent<{ id: string; running: boolean }>).detail;
|
||||
if (running) {
|
||||
if (chatsRef.current?.some((c) => c.id === id)) {
|
||||
setChats((prev) => (prev ? prev.map((c) => (c.id === id ? { ...c, busy: true } : c)) : prev));
|
||||
} else {
|
||||
void refresh(); // the brand-new session: it isn't in the snapshot yet
|
||||
}
|
||||
return;
|
||||
}
|
||||
setChats((prev) => (prev ? prev.map((c) => (c.id === id ? { ...c, busy: false } : c)) : prev));
|
||||
void refresh(); // settle: the server snapshot is now truthful — land it now
|
||||
};
|
||||
window.addEventListener("lh:turn-state", onTurnState);
|
||||
return () => window.removeEventListener("lh:turn-state", onTurnState);
|
||||
}, [refresh]);
|
||||
|
||||
/* ── Keyboard seams (shortcut wave A, 2026-09-17) ── The sidebar owns
|
||||
every session-list interaction, so the shortcuts land here: ⌘⇧F
|
||||
focuses the search field; Alt+↑/↓ cycle the list (opening the sidebar
|
||||
|
||||
Reference in New Issue
Block a user