open-swe/ui/src/lib/agents/queries.ts
Johannes du Plessis 16eb15bab3
fix: show submitted user message immediately in web chat (#1478)
* fix: render submitted user message immediately in web chat

Insert the optimistic prompt into local pendingPrompts state on submit so
the user's message appears right away instead of only after the agent
responds and thread.messages refetches.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: keep queued user prompts in submission order

Offset each pending prompt's insertAt by the number of prompts already
pending so multiple follow-ups queued before the backend echoes them
don't collide at the same splice index and render out of order. Applies
to both the local state and persisted sessionStorage paths.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: roll back optimistic message on send failure, single insertAt source

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-10 09:31:56 -07:00

191 lines
5.3 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { agentsApi } from "./api"
import { addPendingPrompt } from "./pendingPrompts"
import type { ScheduleUpdateRequest } from "./api"
import type { ImageChunk } from "./types"
export const agentThreadKeys = {
all: ["agent-threads"] as const,
detail: (threadId: string) => ["agent-threads", threadId] as const,
}
export const agentScheduleKeys = {
all: ["agent-schedules"] as const,
}
const PREFETCH_THREAD_DETAIL_LIMIT = 12
export function usePrefetchAgentThreadDetails(
threads: Array<{ id: string }>,
activeThreadId?: string
) {
const queryClient = useQueryClient()
useEffect(() => {
const threadIds = threads
.map((thread) => thread.id)
.filter((threadId) => threadId !== activeThreadId)
.slice(0, PREFETCH_THREAD_DETAIL_LIMIT)
threadIds.forEach((threadId) => {
void queryClient.prefetchQuery({
queryKey: agentThreadKeys.detail(threadId),
queryFn: () => agentsApi.getThread(threadId, { markViewed: false }),
})
})
}, [activeThreadId, queryClient, threads])
}
export function useAgentThreads() {
return useQuery({
queryKey: agentThreadKeys.all,
queryFn: () => agentsApi.listThreads(),
refetchInterval: (query) =>
query.state.data?.some((thread) => thread.status === "running")
? 2000
: false,
})
}
export function useAgentThread(threadId: string) {
return useQuery({
queryKey: agentThreadKeys.detail(threadId),
queryFn: () => agentsApi.getThread(threadId),
refetchOnMount: "always",
refetchInterval: (query) => {
const status = query.state.data?.status
return status === "running" ? 2000 : false
},
})
}
export function useAgentSchedules() {
return useQuery({
queryKey: agentScheduleKeys.all,
queryFn: () => agentsApi.listSchedules(),
})
}
export function useCreateAgentSchedule() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: agentsApi.createSchedule,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all })
},
})
}
export function useUpdateAgentSchedule() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (vars: { scheduleId: string; body: ScheduleUpdateRequest }) =>
agentsApi.updateSchedule(vars.scheduleId, vars.body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all })
},
})
}
export function useDeleteAgentSchedule() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: agentsApi.deleteSchedule,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all })
},
})
}
export function useCreateAgentThread() {
const queryClient = useQueryClient()
const navigate = useNavigate()
return useMutation({
mutationFn: agentsApi.createThread,
onSuccess: (thread, variables) => {
addPendingPrompt(
thread.id,
variables.prompt,
thread.messages.length,
variables.images
)
queryClient.setQueryData(agentThreadKeys.detail(thread.id), {
...thread,
status: thread.status === "idle" ? "running" : thread.status,
})
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
navigate({ to: "/agents/$threadId", params: { threadId: thread.id } })
},
})
}
export interface SendAgentMessageVariables {
content: string
images?: Array<ImageChunk>
model_id?: string | null
effort?: string | null
}
export function useSendAgentMessage(threadId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (vars: SendAgentMessageVariables) =>
agentsApi.sendMessage(threadId, {
content: vars.content,
images: vars.images,
model_id: vars.model_id,
effort: vars.effort,
}),
onSuccess: (thread) => {
queryClient.setQueryData(
agentThreadKeys.detail(threadId),
(prev: typeof thread | undefined) => {
if (!prev) return thread
return {
...thread,
messages:
thread.messages.length > 0 ? thread.messages : prev.messages,
}
}
)
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
},
})
}
export function useCancelAgentThread(threadId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => agentsApi.cancelThread(threadId),
onSuccess: (thread) => {
queryClient.setQueryData(agentThreadKeys.detail(threadId), thread)
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
},
})
}
export function useDeleteAgentThread() {
const queryClient = useQueryClient()
const navigate = useNavigate()
return useMutation({
mutationFn: (threadId: string) => agentsApi.deleteThread(threadId),
onSuccess: (_, threadId) => {
queryClient.removeQueries({ queryKey: agentThreadKeys.detail(threadId) })
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
const path = window.location.pathname
if (path.includes(`/agents/${threadId}`)) {
navigate({ to: "/agents" })
}
},
})
}