diff --git a/apps/web/src/components/task-list-sidebar.tsx b/apps/web/src/components/task-list-sidebar.tsx
index 39da7eb5..348dce99 100644
--- a/apps/web/src/components/task-list-sidebar.tsx
+++ b/apps/web/src/components/task-list-sidebar.tsx
@@ -1,5 +1,4 @@
"use client";
-
import {
Archive,
ChevronLeft,
@@ -9,12 +8,11 @@ import {
import { Button } from "@/components/ui/button";
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
import { useQueryState, parseAsString } from "nuqs";
-import { useState } from "react";
+import { useState, useCallback } from "react";
import { ThreadItem } from "./thread-item";
-const THREADS_PER_PAGE = 10; // More threads per page in sidebar
+const THREADS_PER_PAGE = 10;
-// TODO: Clarify Language about Threads and Tasks in the TaskListSidebar component
interface TaskListSidebarProps {
onCollapse?: () => void;
}
@@ -23,15 +21,15 @@ export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
const [currentPage, setCurrentPage] = useState(0);
- const { threads, threadsLoading } = useThreads();
+ const { threads, threadsLoading, handleThreadClick } = useThreads();
- // Handle thread navigation
- const handleThreadClick = (thread: ThreadWithTasks) => {
- setThreadId(thread.thread_id);
- setTaskId(null);
- };
-
- // Sort threads by creation date (newest first) - already done in provider
+ const onThreadClick = useCallback(
+ (thread: ThreadWithTasks) => {
+ handleThreadClick(thread, threadId, setThreadId);
+ },
+ [handleThreadClick, threadId, setThreadId],
+ );
+ // Sort threads by creation date (newest first) TODO use provider (already done there)
const sortedThreads = threads;
const totalThreads = sortedThreads.length;
const totalPages = Math.ceil(totalThreads / THREADS_PER_PAGE);
@@ -77,7 +75,7 @@ export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
))}
diff --git a/apps/web/src/components/task-list.tsx b/apps/web/src/components/task-list.tsx
index 1a132c3d..c420940a 100644
--- a/apps/web/src/components/task-list.tsx
+++ b/apps/web/src/components/task-list.tsx
@@ -1,26 +1,28 @@
"use client";
-
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
import { useQueryState, parseAsString } from "nuqs";
-import { useState } from "react";
+import { useState, useCallback } from "react";
import { ThreadItem } from "./thread-item";
const THREADS_PER_PAGE = 5;
export default function TaskList() {
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
- const [_threadId, setThreadId] = useQueryState("threadId", parseAsString);
+ const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
const [currentPage, setCurrentPage] = useState(0);
- const { threads, threadsLoading } = useThreads();
+ const { threads, threadsLoading, handleThreadClick } = useThreads();
const isDashboardMode = !taskId;
- const handleThreadClick = (thread: ThreadWithTasks) => {
- setThreadId(thread.thread_id);
- setTaskId(null);
- };
+ const onThreadClick = useCallback(
+ (thread: ThreadWithTasks) => {
+ handleThreadClick(thread, threadId, setThreadId);
+ setTaskId(null);
+ },
+ [handleThreadClick, threadId, setThreadId, setTaskId],
+ );
if (!isDashboardMode) {
return null;
@@ -51,7 +53,7 @@ export default function TaskList() {
))}
diff --git a/apps/web/src/components/thread-item.tsx b/apps/web/src/components/thread-item.tsx
index 7b0c19df..e3ac0f8d 100644
--- a/apps/web/src/components/thread-item.tsx
+++ b/apps/web/src/components/thread-item.tsx
@@ -1,11 +1,9 @@
"use client";
-
+import { memo } from "react";
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
-import { Badge } from "@/components/ui/badge";
-import { ThreadWithTasks } from "@/providers/Thread";
+import { ThreadWithTasks, useThreads } from "@/providers/Thread";
import { cn } from "@/lib/utils";
import { StatusIndicator } from "@/components/status-indicator";
-import { Skeleton } from "@/components/ui/skeleton";
import { GitHubSVG } from "./icons/github";
import { useQueryState } from "nuqs";
@@ -16,66 +14,29 @@ interface ThreadItemProps {
className?: string;
}
-export function ThreadItem({
+export const ThreadItem = memo(function ThreadItem({
thread,
onClick,
variant = "dashboard",
className,
}: ThreadItemProps) {
const [threadId] = useQueryState("threadId");
+ const { recentlyUpdatedThreads } = useThreads();
const isSelected = thread.thread_id === threadId;
const isSidebar = variant === "sidebar";
+ const isRecentlyUpdated = recentlyUpdatedThreads.has(thread.thread_id);
const displayDate = new Date(thread.created_at).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
- // Check if thread data is still loading/incomplete
- const isLoading =
- !thread.threadTitle ||
- thread.threadTitle.includes("undefined") ||
- !thread.repository ||
- thread.repository === "Unknown Repository" ||
- thread.repository.includes("undefined");
-
- if (isLoading) {
- return (
-
-
-
-
-
-
-
-
-
- {/* GitHub icon placeholder */}
- {/* Repo */}
- {" "}
- {/* Branch */}
- {/* Date */}
-
-
-
-
-
-
-
-
- );
- }
-
return (
{
@@ -139,4 +100,4 @@ export function ThreadItem({
);
-}
+});
diff --git a/apps/web/src/hooks/useThreadPolling.ts b/apps/web/src/hooks/useThreadPolling.ts
new file mode 100644
index 00000000..042e388e
--- /dev/null
+++ b/apps/web/src/hooks/useThreadPolling.ts
@@ -0,0 +1,47 @@
+import { useEffect, useRef } from "react";
+import { ThreadPoller, PollConfig } from "@/lib/polling/thread-poller";
+import { ThreadWithTasks } from "@/providers/Thread";
+
+interface UseThreadPollingProps {
+ threads: ThreadWithTasks[];
+ getThread: (threadId: string) => Promise;
+ onUpdate: (
+ updatedThreads: ThreadWithTasks[],
+ changedThreadIds: string[],
+ ) => void;
+
+ enabled?: boolean;
+}
+
+export function useThreadPolling({
+ threads,
+ getThread,
+ onUpdate,
+ enabled = true,
+}: UseThreadPollingProps) {
+ const pollerRef = useRef(null);
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ const config: PollConfig = {
+ interval: 15000,
+ onUpdate,
+ };
+
+ pollerRef.current = new ThreadPoller(config, threads, getThread);
+ pollerRef.current.start();
+
+ return () => {
+ if (pollerRef.current) {
+ pollerRef.current.stop();
+ pollerRef.current = null;
+ }
+ };
+ }, [threads, getThread, onUpdate, enabled]);
+
+ return {
+ start: () => pollerRef.current?.start(),
+ stop: () => pollerRef.current?.stop(),
+ };
+}
diff --git a/apps/web/src/lib/polling/thread-poller.ts b/apps/web/src/lib/polling/thread-poller.ts
new file mode 100644
index 00000000..9cfa43b4
--- /dev/null
+++ b/apps/web/src/lib/polling/thread-poller.ts
@@ -0,0 +1,97 @@
+import { ThreadWithTasks } from "@/providers/Thread";
+
+export interface PollConfig {
+ interval: number;
+ onUpdate: (
+ updatedThreads: ThreadWithTasks[],
+ changedThreadIds: string[],
+ ) => void;
+}
+
+export class ThreadPoller {
+ private config: PollConfig;
+ private isPolling: boolean = false;
+ private intervalId: NodeJS.Timeout | null = null;
+ private threads: ThreadWithTasks[];
+ private getThreadFn: (threadId: string) => Promise;
+
+ constructor(
+ config: PollConfig,
+ threads: ThreadWithTasks[],
+ getThreadFn: (threadId: string) => Promise,
+ ) {
+ this.config = config;
+ this.threads = threads;
+ this.getThreadFn = getThreadFn;
+ }
+
+ start(): void {
+ if (this.isPolling) return;
+
+ this.isPolling = true;
+ this.intervalId = setInterval(() => {
+ this.pollThreads();
+ }, this.config.interval);
+ }
+
+ stop(): void {
+ if (!this.isPolling) return;
+
+ this.isPolling = false;
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ }
+
+ private async pollThreads(): Promise {
+ try {
+ const currentThreads = this.threads;
+
+ const threadsToPool = currentThreads.slice(0, 10);
+ const updatedThreads: ThreadWithTasks[] = [];
+ const changedThreadIds: string[] = [];
+ const errors: string[] = [];
+
+ for (const currentThread of threadsToPool) {
+ try {
+ const updatedThread = await this.getThreadFn(currentThread.thread_id);
+ if (updatedThread) {
+ updatedThreads.push(updatedThread);
+
+ if (this.hasThreadChanged(currentThread, updatedThread)) {
+ changedThreadIds.push(updatedThread.thread_id);
+ }
+ }
+ } catch (error) {
+ errors.push(`Thread ${currentThread.thread_id}: ${error}`);
+
+ updatedThreads.push(currentThread);
+ }
+ }
+
+ if (changedThreadIds.length > 0) {
+ this.config.onUpdate(updatedThreads, changedThreadIds);
+ }
+ } catch (error) {
+ console.error("Thread polling error:", error);
+ }
+ }
+
+ private hasThreadChanged(
+ current: ThreadWithTasks,
+ updated: ThreadWithTasks,
+ ): boolean {
+ return (
+ current.completedTasksCount !== updated.completedTasksCount ||
+ current.totalTasksCount !== updated.totalTasksCount ||
+ current.status !== updated.status ||
+ current.threadTitle !== updated.threadTitle ||
+ current.repository !== updated.repository ||
+ current.branch !== updated.branch ||
+ JSON.stringify(current.tasks) !== JSON.stringify(updated.tasks) ||
+ JSON.stringify(current.proposedPlan) !==
+ JSON.stringify(updated.proposedPlan)
+ );
+ }
+}
diff --git a/apps/web/src/providers/Stream.tsx b/apps/web/src/providers/Stream.tsx
index 3a38eaa5..68589eaf 100644
--- a/apps/web/src/providers/Stream.tsx
+++ b/apps/web/src/providers/Stream.tsx
@@ -4,7 +4,6 @@ import React, {
ReactNode,
useState,
useEffect,
- useRef,
} from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
@@ -52,8 +51,7 @@ const StreamSession = ({
githubToken: string;
}) => {
const [threadId, setThreadId] = useQueryState("threadId");
- const { refreshThreads, updateThreadFromStream } = useThreads();
-
+ const { refreshThreads, setThreads } = useThreads();
const streamValue = useTypedStream({
apiUrl,
assistantId,
@@ -69,46 +67,13 @@ const StreamSession = ({
},
onThreadId: (id) => {
setThreadId(id);
- // Refetch threads list when thread ID changes.
- // Wait for some seconds before fetching so we're able to get the new thread that was created.
+
sleep().then(() => {
refreshThreads().catch(console.error);
});
},
});
- // Listen for stream updates to update all thread properties in real-time
- // Use a ref to track the last update to prevent excessive calls
- const lastUpdateRef = useRef<{ threadId: string; valuesHash: string } | null>(
- null,
- );
-
- useEffect(() => {
- if (threadId && streamValue.values) {
- // Create a simple hash of the values to detect actual changes
- const valuesHash = JSON.stringify({
- plan: (streamValue.values as any).plan,
- proposedPlan: (streamValue.values as any).proposedPlan,
- targetRepository: (streamValue.values as any).targetRepository,
- messages: (
- streamValue.values as any
- ).messages?.[0]?.content?.[0]?.text?.substring(0, 50),
- });
-
- // Only update if thread or values actually changed
- if (
- !lastUpdateRef.current ||
- lastUpdateRef.current.threadId !== threadId ||
- lastUpdateRef.current.valuesHash !== valuesHash
- ) {
- lastUpdateRef.current = { threadId, valuesHash };
- updateThreadFromStream(threadId, streamValue.values);
- }
- }
- }, [threadId, streamValue.values, updateThreadFromStream]);
-
- // Real-time updates via stream - no polling needed
-
return (
{children}
@@ -129,7 +94,6 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
const [isAuth, setIsAuth] = useState(null);
const [isLoading, setIsLoading] = useState(false);
- // Initialize from localStorage to prevent modal flash
const [hasGitHubAppInstalled, setHasGitHubAppInstalled] = useState<
boolean | null
>(() => {
@@ -152,7 +116,6 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
useEffect(() => {
if (isAuth) {
- // Check if we already have cached installation status
const cachedInstallationStatus = localStorage.getItem(
"github_app_installed",
);
@@ -165,7 +128,6 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
} else if (cachedInstallationStatus === "false") {
setHasGitHubAppInstalled(false);
} else {
- // Only check installation if we don't have cached status
checkGitHubAppInstallation();
}
}
@@ -189,7 +151,7 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
if (response.ok) {
setHasGitHubAppInstalled(true);
localStorage.setItem("github_app_installed", "true");
- // If the app is installed, fetch a token
+
await fetchGitHubToken();
} else {
const errorData = await response.json();
@@ -197,7 +159,6 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
setHasGitHubAppInstalled(false);
localStorage.setItem("github_app_installed", "false");
} else {
- // If there's a different error, we'll assume the app is not installed
setHasGitHubAppInstalled(false);
localStorage.setItem("github_app_installed", "false");
}
@@ -218,7 +179,7 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
const handleInstallGitHubApp = () => {
setIsLoading(true);
- // Clear cached status so we check again after installation
+
localStorage.removeItem("github_app_installed");
window.location.href = "/api/github/installation";
};
@@ -401,7 +362,6 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
);
};
-// Create a custom hook to use the context
export const useStreamContext = (): StreamContextType => {
const context = useContext(StreamContext);
if (context === undefined) {
diff --git a/apps/web/src/providers/Thread.tsx b/apps/web/src/providers/Thread.tsx
index f90c70d5..61429cb0 100644
--- a/apps/web/src/providers/Thread.tsx
+++ b/apps/web/src/providers/Thread.tsx
@@ -9,10 +9,12 @@ import {
Dispatch,
SetStateAction,
useEffect,
+ useTransition,
} from "react";
import { createClient } from "./client";
import { getMessageContentString } from "@open-swe/shared/messages";
-import { TaskPlan } from "@open-swe/shared/open-swe/types";
+import { TaskPlan, GraphState } from "@open-swe/shared/open-swe/types";
+import { useThreadPolling } from "@/hooks/useThreadPolling";
export interface ThreadWithTasks extends Thread {
threadTitle: string;
@@ -31,7 +33,13 @@ interface ThreadContextType {
setThreadsLoading: Dispatch>;
refreshThreads: () => Promise;
getThread: (threadId: string) => Promise;
- updateThreadFromStream: (threadId: string, streamValues: any) => void;
+ isPending: boolean;
+ recentlyUpdatedThreads: Set;
+ handleThreadClick: (
+ thread: ThreadWithTasks,
+ currentThreadId: string | null,
+ setThreadId: (id: string) => void,
+ ) => void;
}
const ThreadContext = createContext(undefined);
@@ -49,39 +57,56 @@ function getThreadSearchMetadata(
const getTaskCounts = (
tasks?: TaskPlan,
proposedPlan?: string[],
+ existingCounts?: { totalTasksCount: number; completedTasksCount: number },
): { totalTasksCount: number; completedTasksCount: number } => {
- if (proposedPlan && !tasks) {
+ const defaultCounts = existingCounts || {
+ totalTasksCount: 0,
+ completedTasksCount: 0,
+ };
+
+ if (proposedPlan && proposedPlan.length > 0 && !tasks) {
return {
totalTasksCount: proposedPlan.length,
completedTasksCount: 0,
};
}
- if (!tasks) {
- // No tasks passed, return 0s
- return { totalTasksCount: 0, completedTasksCount: 0 };
+ if (!tasks || !tasks.tasks || tasks.tasks.length === 0) {
+ return defaultCounts;
+ }
+ const activeTaskIndex = tasks.activeTaskIndex;
+ const activeTask = tasks.tasks.find(
+ (task) => task.taskIndex === activeTaskIndex,
+ );
+
+ if (
+ !activeTask ||
+ !activeTask.planRevisions ||
+ activeTask.planRevisions.length === 0
+ ) {
+ return defaultCounts;
}
- const activeTaskList = tasks.tasks.find(
- (t) => t.taskIndex === tasks.activeTaskIndex,
+ const activeRevisionIndex = activeTask.activeRevisionIndex;
+ const activeRevision = activeTask.planRevisions.find(
+ (revision) => revision.revisionIndex === activeRevisionIndex,
);
- if (!activeTaskList) {
- // Something is wrong here. Return 0
- return { totalTasksCount: 0, completedTasksCount: 0 };
+
+ if (
+ !activeRevision ||
+ !activeRevision.plans ||
+ activeRevision.plans.length === 0
+ ) {
+ return defaultCounts;
}
- const activeTaskPlans = activeTaskList.planRevisions.find(
- (p) => p.revisionIndex === activeTaskList.activeRevisionIndex,
- );
- if (!activeTaskPlans) {
- // Something is wrong here. Return 0
- return { totalTasksCount: 0, completedTasksCount: 0 };
- }
+ const plans = activeRevision.plans;
+
+ const completedTasksCount = plans.filter((p) => p.completed)?.length || 0;
return {
- totalTasksCount: activeTaskPlans.plans.length,
- completedTasksCount: activeTaskPlans.plans.filter((p) => p.completed)
- .length,
+ totalTasksCount: plans.length,
+ completedTasksCount,
};
};
@@ -92,63 +117,10 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
const [threads, setThreads] = useState([]);
const [threadsLoading, setThreadsLoading] = useState(false);
- const [busyThreadIds, setBusyThreadIds] = useState([]);
-
- // Real-time thread updater for all properties (replaces polling)
- const updateThreadFromStream = useCallback(
- (threadId: string, streamValues: any) => {
- if (!threadId || !streamValues) return;
-
- setThreads((currentThreads) => {
- const targetThread = currentThreads.find(
- (t) => t.thread_id === threadId,
- );
- if (!targetThread) return currentThreads; // Thread not found, no update needed
-
- const plan: TaskPlan | undefined = streamValues?.plan;
- const proposedPlan: string[] = streamValues?.proposedPlan || [];
- const targetRepository = streamValues?.targetRepository;
- const messages = streamValues?.messages;
-
- const { totalTasksCount, completedTasksCount } = getTaskCounts(
- plan,
- proposedPlan,
- );
-
- // Extract thread title from messages if available
- const firstMessageContent = messages?.[0]?.content;
- const threadTitle = firstMessageContent
- ? getMessageContentString(firstMessageContent)
- : targetThread.threadTitle;
-
- const newRepository =
- targetRepository?.repo ||
- targetRepository?.name ||
- targetThread.repository ||
- "Unknown Repository";
-
- const newBranch =
- targetRepository?.branch || targetThread.branch || "main";
-
- return currentThreads.map((thread) => {
- if (thread.thread_id === threadId) {
- return {
- ...thread,
- threadTitle,
- repository: newRepository,
- branch: newBranch,
- completedTasksCount,
- totalTasksCount,
- tasks: plan,
- proposedPlan,
- };
- }
- return thread;
- });
- });
- },
- [],
- );
+ const [isPending, startTransition] = useTransition();
+ const [recentlyUpdatedThreads, setRecentlyUpdatedThreads] = useState<
+ Set
+ >(new Set());
const getThread = useCallback(
async (threadId: string): Promise => {
@@ -157,7 +129,17 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
try {
const thread = await client.threads.get(threadId);
- return enhanceThreadWithTasks(thread);
+
+ // Get the comprehensive state data which contains the plan
+ let stateData: { values: GraphState } | null = null;
+
+ try {
+ stateData = await client.threads.getState(threadId);
+ } catch (stateError) {
+ console.error("Failed to get state data:", stateError);
+ }
+
+ return enhanceThreadWithTasks(thread, stateData);
} catch (error) {
console.error("Failed to fetch thread:", threadId, error);
return null;
@@ -166,13 +148,20 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
[apiUrl, assistantId],
);
- const enhanceThreadWithTasks = (thread: Thread): ThreadWithTasks => {
- const threadValues = thread.values as any;
- const plan: TaskPlan | undefined = threadValues?.plan;
- const proposedPlan: string[] = threadValues?.proposedPlan || [];
+ const enhanceThreadWithTasks = (
+ thread: Thread,
+ stateData?: { values: GraphState } | null,
+ ): ThreadWithTasks => {
+ const stateValues = stateData?.values;
+ const threadValues = thread.values as GraphState;
- const targetRepository = threadValues?.targetRepository;
- const messages = (threadValues as any)?.messages;
+ const plan: TaskPlan | undefined = stateValues?.plan || threadValues?.plan;
+ const proposedPlan: string[] =
+ stateValues?.proposedPlan || threadValues?.proposedPlan || [];
+
+ const targetRepository =
+ stateValues?.targetRepository || threadValues?.targetRepository;
+ const messages = stateValues?.messages || threadValues?.messages;
const firstMessageContent = messages?.[0]?.content;
const threadTitle = firstMessageContent
? getMessageContentString(firstMessageContent)
@@ -186,10 +175,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
return {
...thread,
threadTitle,
- repository:
- targetRepository?.repo ||
- targetRepository?.name ||
- "Unknown Repository",
+ repository: targetRepository?.repo || "Unknown Repository",
branch: targetRepository?.branch || "main",
completedTasksCount,
totalTasksCount,
@@ -205,7 +191,6 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
const client = createClient(apiUrl);
try {
- // Simple thread search - try both metadata approaches
const searchParams = {
limit: 100,
metadata: getThreadSearchMetadata(assistantId),
@@ -213,7 +198,6 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
let threadsResponse = await client.threads.search(searchParams);
- // If no threads found, try alternative metadata
if (threadsResponse.length === 0) {
const altMetadata = assistantId.includes("-")
? { assistant_id: assistantId }
@@ -224,18 +208,24 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
});
}
- // Enhance threads with task data
const enhancedThreads: ThreadWithTasks[] = [];
for (const thread of threadsResponse) {
try {
const fullThread = await client.threads.get(thread.thread_id);
- enhancedThreads.push(enhanceThreadWithTasks(fullThread));
+
+ let stateData: { values: GraphState } | null = null;
+ try {
+ stateData = await client.threads.getState(thread.thread_id);
+ } catch (stateError) {
+ console.error("Failed to get state data:", stateError);
+ }
+
+ enhancedThreads.push(enhanceThreadWithTasks(fullThread, stateData));
} catch (error) {
console.error(`Failed to enhance thread ${thread.thread_id}:`, error);
}
}
- // Sort by creation date (newest first)
enhancedThreads.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
@@ -249,13 +239,49 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
}
}, [apiUrl, assistantId]);
- // Removed polling - now using real-time stream updates via updateThreadFromStream
-
- // Initial load
useEffect(() => {
refreshThreads();
}, [refreshThreads]);
+ const handlePollingUpdate = useCallback(
+ (updatedThreads: ThreadWithTasks[], changedThreadIds: string[]) => {
+ setThreads((currentThreads) => {
+ const updatedMap = new Map(updatedThreads.map((t) => [t.thread_id, t]));
+ return currentThreads.map(
+ (thread) => updatedMap.get(thread.thread_id) || thread,
+ );
+ });
+
+ setRecentlyUpdatedThreads(new Set(changedThreadIds));
+
+ setTimeout(() => {
+ setRecentlyUpdatedThreads(new Set());
+ }, 2000);
+ },
+ [],
+ );
+
+ // Initialize polling
+ useThreadPolling({
+ threads,
+ getThread,
+ onUpdate: handlePollingUpdate,
+ enabled: true,
+ });
+
+ const handleThreadClick = useCallback(
+ (
+ thread: ThreadWithTasks,
+ currentThreadId: string | null,
+ setThreadId: (id: string) => void,
+ ) => {
+ if (currentThreadId === thread.thread_id) return;
+
+ setThreadId(thread.thread_id);
+ },
+ [],
+ );
+
const value = {
threads,
setThreads,
@@ -263,7 +289,9 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
setThreadsLoading,
refreshThreads,
getThread,
- updateThreadFromStream,
+ isPending,
+ recentlyUpdatedThreads,
+ handleThreadClick,
};
return (