fix(frontend): preserve agent context in thread history routes (#1771)
* fix(frontend): preserve agent context in thread history routes * fix(frontend): preserve agent thread fallback context * style(frontend): format thread route utils test --------- Co-authored-by: luoxiao6645 <luoxiao6645@gmail.com>
This commit is contained in:
parent
616caa92b1
commit
60e0abfdb8
|
|
@ -48,10 +48,7 @@ export default function ChatsPage() {
|
||||||
<ScrollArea className="size-full py-4">
|
<ScrollArea className="size-full py-4">
|
||||||
<div className="mx-auto flex size-full max-w-(--container-width-md) flex-col">
|
<div className="mx-auto flex size-full max-w-(--container-width-md) flex-col">
|
||||||
{filteredThreads?.map((thread) => (
|
{filteredThreads?.map((thread) => (
|
||||||
<Link
|
<Link key={thread.thread_id} href={pathOfThread(thread)}>
|
||||||
key={thread.thread_id}
|
|
||||||
href={pathOfThread(thread.thread_id)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2 border-b p-4">
|
<div className="flex flex-col gap-2 border-b p-4">
|
||||||
<div>
|
<div>
|
||||||
<div>{titleOfThread(thread)}</div>
|
<div>{titleOfThread(thread)}</div>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,11 @@ export function RecentChatList() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
const { thread_id: threadIdFromPath, agent_name: agentNameFromPath } =
|
||||||
|
useParams<{
|
||||||
|
thread_id: string;
|
||||||
|
agent_name?: string;
|
||||||
|
}>();
|
||||||
const { data: threads = [] } = useThreads();
|
const { data: threads = [] } = useThreads();
|
||||||
const { mutate: deleteThread } = useDeleteThread();
|
const { mutate: deleteThread } = useDeleteThread();
|
||||||
const { mutate: renameThread } = useRenameThread();
|
const { mutate: renameThread } = useRenameThread();
|
||||||
|
|
@ -77,18 +81,20 @@ export function RecentChatList() {
|
||||||
deleteThread({ threadId });
|
deleteThread({ threadId });
|
||||||
if (threadId === threadIdFromPath) {
|
if (threadId === threadIdFromPath) {
|
||||||
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
||||||
let nextThreadId = "new";
|
let nextThreadPath = pathOfThread("new", {
|
||||||
|
agent_name: agentNameFromPath,
|
||||||
|
});
|
||||||
if (threadIndex > -1) {
|
if (threadIndex > -1) {
|
||||||
if (threads[threadIndex + 1]) {
|
if (threads[threadIndex + 1]) {
|
||||||
nextThreadId = threads[threadIndex + 1]!.thread_id;
|
nextThreadPath = pathOfThread(threads[threadIndex + 1]!);
|
||||||
} else if (threads[threadIndex - 1]) {
|
} else if (threads[threadIndex - 1]) {
|
||||||
nextThreadId = threads[threadIndex - 1]!.thread_id;
|
nextThreadPath = pathOfThread(threads[threadIndex - 1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void router.push(`/workspace/chats/${nextThreadId}`);
|
void router.push(nextThreadPath);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteThread, router, threadIdFromPath, threads],
|
[agentNameFromPath, deleteThread, router, threadIdFromPath, threads],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRenameClick = useCallback(
|
const handleRenameClick = useCallback(
|
||||||
|
|
@ -110,7 +116,7 @@ export function RecentChatList() {
|
||||||
}, [renameThread, renameThreadId, renameValue]);
|
}, [renameThread, renameThreadId, renameValue]);
|
||||||
|
|
||||||
const handleShare = useCallback(
|
const handleShare = useCallback(
|
||||||
async (threadId: string) => {
|
async (thread: AgentThread) => {
|
||||||
// Always use Vercel URL for sharing so others can access
|
// Always use Vercel URL for sharing so others can access
|
||||||
const VERCEL_URL = "https://deer-flow-v2.vercel.app";
|
const VERCEL_URL = "https://deer-flow-v2.vercel.app";
|
||||||
const isLocalhost =
|
const isLocalhost =
|
||||||
|
|
@ -118,7 +124,7 @@ export function RecentChatList() {
|
||||||
window.location.hostname === "127.0.0.1";
|
window.location.hostname === "127.0.0.1";
|
||||||
// On localhost: use Vercel URL; On production: use current origin
|
// On localhost: use Vercel URL; On production: use current origin
|
||||||
const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin;
|
const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin;
|
||||||
const shareUrl = `${baseUrl}/workspace/chats/${threadId}`;
|
const shareUrl = `${baseUrl}${pathOfThread(thread)}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
toast.success(t.clipboard.linkCopied);
|
toast.success(t.clipboard.linkCopied);
|
||||||
|
|
@ -169,7 +175,7 @@ export function RecentChatList() {
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<div className="flex w-full flex-col gap-1">
|
<div className="flex w-full flex-col gap-1">
|
||||||
{threads.map((thread) => {
|
{threads.map((thread) => {
|
||||||
const isActive = pathOfThread(thread.thread_id) === pathname;
|
const isActive = pathOfThread(thread) === pathname;
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem
|
<SidebarMenuItem
|
||||||
key={thread.thread_id}
|
key={thread.thread_id}
|
||||||
|
|
@ -179,7 +185,7 @@ export function RecentChatList() {
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
className="text-muted-foreground block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
|
className="text-muted-foreground block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
|
||||||
href={pathOfThread(thread.thread_id)}
|
href={pathOfThread(thread)}
|
||||||
>
|
>
|
||||||
{titleOfThread(thread)}
|
{titleOfThread(thread)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -211,7 +217,7 @@ export function RecentChatList() {
|
||||||
<span>{t.common.rename}</span>
|
<span>{t.common.rename}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => handleShare(thread.thread_id)}
|
onSelect={() => handleShare(thread)}
|
||||||
>
|
>
|
||||||
<Share2 className="text-muted-foreground" />
|
<Share2 className="text-muted-foreground" />
|
||||||
<span>{t.common.share}</span>
|
<span>{t.common.share}</span>
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,7 @@ export function useThreads(
|
||||||
limit: 50,
|
limit: 50,
|
||||||
sortBy: "updated_at",
|
sortBy: "updated_at",
|
||||||
sortOrder: "desc",
|
sortOrder: "desc",
|
||||||
select: ["thread_id", "updated_at", "values"],
|
select: ["thread_id", "updated_at", "values", "context"],
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const apiClient = getAPIClient();
|
const apiClient = getAPIClient();
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ export interface AgentThreadState extends Record<string, unknown> {
|
||||||
todos?: Todo[];
|
todos?: Todo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentThread extends Thread<AgentThreadState> {}
|
|
||||||
|
|
||||||
export interface AgentThreadContext extends Record<string, unknown> {
|
export interface AgentThreadContext extends Record<string, unknown> {
|
||||||
thread_id: string;
|
thread_id: string;
|
||||||
model_name: string | undefined;
|
model_name: string | undefined;
|
||||||
|
|
@ -20,3 +18,7 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
||||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||||
agent_name?: string;
|
agent_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentThread extends Thread<AgentThreadState> {
|
||||||
|
context?: AgentThreadContext;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const { pathOfThread } = await import(
|
||||||
|
new URL("./utils.ts", import.meta.url).href
|
||||||
|
);
|
||||||
|
|
||||||
|
void test("uses standard chat route when thread has no agent context", () => {
|
||||||
|
assert.equal(pathOfThread("thread-123"), "/workspace/chats/thread-123");
|
||||||
|
assert.equal(
|
||||||
|
pathOfThread({
|
||||||
|
thread_id: "thread-123",
|
||||||
|
}),
|
||||||
|
"/workspace/chats/thread-123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("uses agent chat route when thread context has agent_name", () => {
|
||||||
|
assert.equal(
|
||||||
|
pathOfThread({
|
||||||
|
thread_id: "thread-123",
|
||||||
|
context: { agent_name: "researcher" },
|
||||||
|
}),
|
||||||
|
"/workspace/agents/researcher/chats/thread-123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("uses provided context when pathOfThread is called with a thread id", () => {
|
||||||
|
assert.equal(
|
||||||
|
pathOfThread("thread-123", { agent_name: "ops agent" }),
|
||||||
|
"/workspace/agents/ops%20agent/chats/thread-123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,28 @@
|
||||||
import type { Message } from "@langchain/langgraph-sdk";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
import type { AgentThread } from "./types";
|
import type { AgentThread, AgentThreadContext } from "./types";
|
||||||
|
|
||||||
export function pathOfThread(threadId: string) {
|
type ThreadRouteTarget =
|
||||||
return `/workspace/chats/${threadId}`;
|
| string
|
||||||
|
| Pick<AgentThread, "thread_id" | "context">
|
||||||
|
| {
|
||||||
|
thread_id: string;
|
||||||
|
context?: Pick<AgentThreadContext, "agent_name"> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pathOfThread(
|
||||||
|
thread: ThreadRouteTarget,
|
||||||
|
context?: Pick<AgentThreadContext, "agent_name"> | null,
|
||||||
|
) {
|
||||||
|
const threadId = typeof thread === "string" ? thread : thread.thread_id;
|
||||||
|
const agentName =
|
||||||
|
typeof thread === "string"
|
||||||
|
? context?.agent_name
|
||||||
|
: thread.context?.agent_name;
|
||||||
|
|
||||||
|
return agentName
|
||||||
|
? `/workspace/agents/${encodeURIComponent(agentName)}/chats/${threadId}`
|
||||||
|
: `/workspace/chats/${threadId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textOfMessage(message: Message) {
|
export function textOfMessage(message: Message) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue