diff --git a/app-prefixable/src/context/providers.tsx b/app-prefixable/src/context/providers.tsx index 29e70d76..25a6f02d 100644 --- a/app-prefixable/src/context/providers.tsx +++ b/app-prefixable/src/context/providers.tsx @@ -294,6 +294,19 @@ export function ProviderProvider(props: ParentProps) { const targetProvider = hasValidConfigModel ? parsedProvider : FALLBACK_PROVIDER const targetModel = hasValidConfigModel ? parsedModel : FALLBACK_MODEL + const stale = Object.entries(store.modelsByAgent) + .filter(([, model]) => { + const provider = data.all.find((p) => p.id === model.providerID) + return !data.connected.includes(model.providerID) || !provider?.models[model.modelID] + }) + .map(([agent]) => agent) + if (stale.length > 0) { + setStore("modelsByAgent", produce((models) => { + for (const agent of stale) delete models[agent] + })) + return + } + // Only auto-set model when there is no existing selection for this agent // (localStorage or previous user choice). This prevents overriding user selections. if (!store.modelsByAgent[defaultAgent]) { diff --git a/app-prefixable/src/context/sync.tsx b/app-prefixable/src/context/sync.tsx index 0f4024a3..0f7628c6 100644 --- a/app-prefixable/src/context/sync.tsx +++ b/app-prefixable/src/context/sync.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, createSignal, onCleanup, batch, type ParentProps } from "solid-js" import { createStore, reconcile, produce } from "solid-js/store" -import type { Session, Message, Part, Provider } from "../sdk/client" +import type { Session, Message, Part } from "../sdk/client" import { useSDK } from "./sdk" import { useServer } from "./server" import { createSSEParser, nextSSEReconnectDelay } from "../utils/sse" @@ -18,12 +18,6 @@ export type MessageWithParts = { parts: Part[] } -type ProviderData = { - all: Provider[] - connected: string[] - default: Record -} - type SyncStore = { ready: boolean error: string | null @@ -31,7 +25,6 @@ type SyncStore = { archivedSession: Session[] message: Record part: Record - provider: ProviderData } interface SyncContextValue { @@ -42,7 +35,6 @@ interface SyncContextValue { archivedSessions: () => Session[] messages: (sessionID: string) => MessageWithParts[] parts: (messageID: string) => Part[] - providers: () => ProviderData sseUnhealthy: () => boolean subscribe: (handler: SyncEventHandler) => () => void session: { @@ -200,7 +192,6 @@ export function SyncProvider(props: ParentProps) { archivedSession: [], message: {}, part: {}, - provider: { all: [], connected: [], default: {} }, }) const inflight = new Map>() @@ -449,14 +440,6 @@ export function SyncProvider(props: ParentProps) { } } - // Provider events - if (event.type === "provider.updated") { - const data = props as unknown as ProviderData - if (data) { - setStore("provider", data) - } - } - for (const handler of handlers) { try { handler(event) @@ -474,7 +457,7 @@ export function SyncProvider(props: ParentProps) { async function bootstrap() { setStore("error", null) try { - const [sessionsRes, providersRes] = await Promise.all([client.session.list(), client.provider.list()]) + const sessionsRes = await client.session.list() batch(() => { const rawSessions = sessionsRes.data ?? [] @@ -484,10 +467,6 @@ export function SyncProvider(props: ParentProps) { setStore("session", reconcile(sessions, { key: "id" })) setStore("archivedSession", reconcile(archived, { key: "id" })) - if (providersRes.data) { - setStore("provider", providersRes.data as unknown as ProviderData) - } - setStore("ready", true) }) @@ -596,7 +575,6 @@ export function SyncProvider(props: ParentProps) { archivedSessions: () => store.archivedSession, messages: (sessionID: string) => store.message[sessionID] ?? [], parts: (messageID: string) => store.part[messageID] ?? [], - providers: () => store.provider, sseUnhealthy, subscribe, session: { diff --git a/app-prefixable/src/pages/layout.tsx b/app-prefixable/src/pages/layout.tsx index be545da2..1e63a254 100644 --- a/app-prefixable/src/pages/layout.tsx +++ b/app-prefixable/src/pages/layout.tsx @@ -1159,43 +1159,25 @@ export function Layout(props: ParentProps) { }); } - function errorText(err: unknown) { - if (err instanceof Error && err.message.trim()) return err.message; - return "Session bootstrap failed. Check API connectivity and retry."; - } - - let lastSessionsLoadAt = 0; - - async function loadSessions() { - lastSessionsLoadAt = Date.now(); - try { - const res = await client.session.list({ roots: true }); - const data = res.data; - if (Array.isArray(data)) { - const valid = data.filter( - (s): s is Session => - s && typeof s === "object" && typeof s.id === "string", - ); - setSessions(valid); - setSessionLoadError(null); - } else { - setSessions([]); - setSessionLoadError(null); - } - } catch (e) { - console.error("Failed to load sessions:", e); + createEffect(() => { + const error = sync.bootstrapError; + if (!sync.ready && !error) return; + if (error) { setSessions([]); - setSessionLoadError(errorText(e)); - } finally { + setSessionLoadError(error); setLoading(false); + return; } - } + setSessions(sync.sessions().filter((session) => !session.parentID)); + setSessionLoadError(null); + setLoading(false); + }); const sessionError = createMemo(() => sessionLoadError()); function retrySessionBootstrap() { setLoading(true); - void loadSessions(); + void sync.refresh(); } function handleSearchInput(query: string) { @@ -1584,38 +1566,6 @@ export function Layout(props: ParentProps) { }); }); - onMount(() => { - loadSessions(); - - let sessionsTimer: number | undefined; - const unsub = events.subscribe((event) => { - if (event.type === "server.connected") { - if (Date.now() - lastSessionsLoadAt < 5000) return; - if (sessionsTimer !== undefined) clearTimeout(sessionsTimer); - sessionsTimer = window.setTimeout(() => { - sessionsTimer = undefined; - loadSessions(); - }, 500); - return; - } - if ( - event.type === "session.created" || - event.type === "session.updated" || - event.type === "session.deleted" - ) { - // Guard against child sessions — sidebar only shows root sessions - const info = (event.properties as { info: { parentID?: string } }).info; - if (info?.parentID) return; - loadSessions(); - } - }); - - onCleanup(() => { - unsub(); - if (sessionsTimer !== undefined) clearTimeout(sessionsTimer); - }); - }); - onMount(() => { const unsub = events.subscribe((event) => { if (event.type === "worktree.ready") { diff --git a/app-prefixable/src/pages/session.tsx b/app-prefixable/src/pages/session.tsx index 41b85958..3f907489 100644 --- a/app-prefixable/src/pages/session.tsx +++ b/app-prefixable/src/pages/session.tsx @@ -825,7 +825,9 @@ export function Session() { } sync.session.sync(id).then((synced) => { if (state.stopped || sessionId() !== id || !synced) return; - if (assistantFinished(id)) finishProcessing(); + finishProcessing(); + if (assistantFinished(id)) return; + setError("Session stopped before the assistant responded. Verify the selected model and retry."); }); } }) @@ -865,7 +867,9 @@ export function Session() { if (polled) return; sync.session.sync(id).then((synced) => { if (sessionId() !== id || !synced) return; - if (assistantFinished(id)) finishProcessing(); + finishProcessing(); + if (assistantFinished(id)) return; + setError("Session stopped before the assistant responded. Verify the selected model and retry."); }); }) .catch((err) => console.warn("[Session] Watchdog poll failed:", err));