diff --git a/src/app/components/FileViewDialog.tsx b/src/app/components/FileViewDialog.tsx index 6ba63a4..0de3fc9 100644 --- a/src/app/components/FileViewDialog.tsx +++ b/src/app/components/FileViewDialog.tsx @@ -138,7 +138,10 @@ export const FileViewDialog = React.memo<{ open={true} onOpenChange={onClose} > - + {file?.path || "New File"} diff --git a/src/app/components/SkillDetailDialog.tsx b/src/app/components/SkillDetailDialog.tsx index 8def5c4..0c61097 100644 --- a/src/app/components/SkillDetailDialog.tsx +++ b/src/app/components/SkillDetailDialog.tsx @@ -72,7 +72,10 @@ export const SkillDetailDialog = React.memo<{ open={true} onOpenChange={onClose} > - + {title}
- + {path}
diff --git a/src/app/hooks/useThreads.ts b/src/app/hooks/useThreads.ts index fb85020..a546b64 100644 --- a/src/app/hooks/useThreads.ts +++ b/src/app/hooks/useThreads.ts @@ -101,7 +101,10 @@ export function useThreads(props: { try { if (thread.values && typeof thread.values === "object") { const values = thread.values as any; - const firstHumanMessage = values.messages.find( + const messages: any[] = Array.isArray(values.messages) + ? values.messages + : []; + const firstHumanMessage = messages.find( (m: any) => m.type === "human" ); if (firstHumanMessage?.content) { @@ -111,15 +114,26 @@ export function useThreads(props: { : firstHumanMessage.content[0]?.text || ""; title = content.slice(0, 50) + (content.length > 50 ? "..." : ""); } - const firstAiMessage = values.messages.find( - (m: any) => m.type === "ai" - ); - if (firstAiMessage?.content) { - const content = - typeof firstAiMessage.content === "string" - ? firstAiMessage.content - : firstAiMessage.content[0]?.text || ""; - description = content.slice(0, 100); + // Preview = the first AI message that actually has text. Agentic + // threads often open with tool-call-only AI messages (empty + // content), so picking the literal first AI message would leave the + // row blank (looking like an "empty" thread). Also join all text + // parts rather than just content[0]. + const aiText = (content: any): string => { + if (typeof content === "string") return content; + if (Array.isArray(content)) + return content + .map((p: any) => (typeof p?.text === "string" ? p.text : "")) + .join(""); + return ""; + }; + for (const m of messages) { + if (m?.type !== "ai") continue; + const t = aiText(m.content).trim(); + if (t) { + description = t.slice(0, 100); + break; + } } } } catch { diff --git a/src/app/page.tsx b/src/app/page.tsx index 2cc017d..b7e3d39 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -52,58 +52,54 @@ function HomePageInner({ config.assistantId ); - if (isUUID) { - // We should try to fetch the assistant directly with this UUID - try { - const data = await client.assistants.get(config.assistantId); - setAssistant(data); - } catch (error) { - console.error("Failed to fetch assistant:", error); - setAssistant({ - assistant_id: config.assistantId, - graph_id: config.assistantId, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - config: {}, - metadata: {}, - version: 1, - name: "Assistant", - context: {}, - }); + const resolve = async (): Promise => { + // A UUID addresses one assistant directly; otherwise list the graph's + // assistants and prefer the system default (fall back to the first). + if (isUUID) { + return await client.assistants.get(config.assistantId); } - } else { + const assistants = await client.assistants.search({ + graphId: config.assistantId, + limit: 100, + }); + const found = + assistants.find((a) => a.metadata?.["created_by"] === "system") ?? + assistants[0]; + if (!found) throw new Error("No assistant found for this graph."); + return found; + }; + + // The langgraph backend may not be ready the instant the page mounts — the + // request then fails with "Failed to fetch". Retry a few times so a transient + // startup race self-heals instead of surfacing a scary console error. + for (let attempt = 0; attempt < 3; attempt += 1) { try { - // We should try to list out the assistants for this graph, and then use the default one. - // TODO: Paginate this search, but 100 should be enough for graph name - const assistants = await client.assistants.search({ - graphId: config.assistantId, - limit: 100, - }); - const defaultAssistant = assistants.find( - (assistant) => assistant.metadata?.["created_by"] === "system" - ); - if (defaultAssistant === undefined) { - throw new Error("No default assistant found"); - } - setAssistant(defaultAssistant); + setAssistant(await resolve()); + return; } catch (error) { - console.error( - "Failed to find default assistant from graph_id: try setting the assistant_id directly:", + if (attempt < 2) { + await new Promise((r) => setTimeout(r, 700)); + continue; + } + console.warn( + "Couldn't resolve the EvoScientist assistant; addressing the graph by id instead. Is the backend running?", error ); - setAssistant({ - assistant_id: config.assistantId, - graph_id: config.assistantId, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - config: {}, - metadata: {}, - version: 1, - name: config.assistantId, - context: {}, - }); } } + + // Fallback: address the graph directly by id (works on `langgraph dev`). + setAssistant({ + assistant_id: config.assistantId, + graph_id: config.assistantId, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + config: {}, + metadata: {}, + version: 1, + name: config.assistantId, + context: {}, + }); }, [client, config.assistantId]); useEffect(() => {