Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app-prefixable/src/context/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
26 changes: 2 additions & 24 deletions app-prefixable/src/context/sync.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -18,20 +18,13 @@ export type MessageWithParts = {
parts: Part[]
}

type ProviderData = {
all: Provider[]
connected: string[]
default: Record<string, string>
}

type SyncStore = {
ready: boolean
error: string | null
session: Session[]
archivedSession: Session[]
message: Record<string, MessageWithParts[]>
part: Record<string, Part[]>
provider: ProviderData
}

interface SyncContextValue {
Expand All @@ -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: {
Expand Down Expand Up @@ -200,7 +192,6 @@ export function SyncProvider(props: ParentProps) {
archivedSession: [],
message: {},
part: {},
provider: { all: [], connected: [], default: {} },
})

const inflight = new Map<string, Promise<boolean>>()
Expand Down Expand Up @@ -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)
Expand All @@ -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 ?? []
Expand All @@ -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)
})

Expand Down Expand Up @@ -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: {
Expand Down
72 changes: 11 additions & 61 deletions app-prefixable/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
Comment on lines +1162 to 1166
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) {
Expand Down Expand Up @@ -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") {
Expand Down
8 changes: 6 additions & 2 deletions app-prefixable/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
});
}
})
Expand Down Expand Up @@ -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));
Expand Down