diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index b4508e06..91aa89cf 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -61,7 +61,6 @@ export default function ChatPage() { setIsNewThread, isMock, showWelcomeStyle, - invalidNewRoute, } = useThreadChat(); // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/xclaw_used 参数。 @@ -119,7 +118,6 @@ export default function ChatPage() { }, [thread.values?.title]); const [hasSubmitted, setHasSubmitted] = useState(false); - const showInputBox = !invalidNewRoute && !(showWelcomeStyle && thread.isThreadLoading); const [historyCutoff, setHistoryCutoff] = useState(null); useEffect(() => { @@ -214,12 +212,10 @@ export default function ChatPage() { setArtifactsOpen, setIsNewThread, ]); - // shouldRenderHistory || historyCutoff === null - // console.log('shouldRenderHistory', shouldRenderHistory, 'historyCutoff', historyCutoff); return ( - +
- {invalidNewRoute ? ( -
-
-

- 缺少 thread_id 参数 -

-

- 访问 - /workspace/chats/new - 时必须显式传入 - ?thread_id=... - ,当前页面不会继续使用本地缓存兜底。 -

-
-
- ) : ( - - )} +
@@ -433,7 +408,7 @@ export default function ChatPage() { showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]", )} > - {showInputBox ? ( + {!(showWelcomeStyle && thread.isThreadLoading) ? ( { diff --git a/frontend/src/components/workspace/artifacts/artifact-file-list.tsx b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx index c36d94f8..163ab44d 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-list.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx @@ -29,7 +29,7 @@ export function ArtifactFileList({ }: { className?: string; files: string[]; - threadId?: string; + threadId: string; }) { const { t } = useI18n(); const { select: selectArtifact, setOpen } = useArtifacts(); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index 2e607197..eed2754d 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -7,7 +7,9 @@ import { resolveThreadQueryIntent } from "@/core/threads/utils"; export function useThreadChat() { const pathname = usePathname(); - const params = useParams<{ thread_id?: string }>(); + const params = useParams<{ thread_id: string }>(); + const searchParams = useSearchParams(); + const threadIdFromSearchParams = searchParams.get("thread_id")?.trim(); // 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。 const threadIdFromPathname = (() => { const parts = pathname.split("?")[0]?.split("/") ?? []; @@ -18,8 +20,11 @@ export function useThreadChat() { return undefined; })(); const rawPathThreadId = params?.thread_id ?? threadIdFromPathname; + const isNewRoute = rawPathThreadId === "new"; - const threadIdFromPath = isNewRoute ? undefined : rawPathThreadId; + const threadIdFromPathOrParams:string = isNewRoute + ? threadIdFromSearchParams?? params.thread_id + : params.thread_id; // console.log("[useThreadChat] pathname", pathname); // console.log("[useThreadChat] params.thread_id", params?.thread_id); // console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname); @@ -33,10 +38,9 @@ export function useThreadChat() { return isValidThreadId(stored) ? stored : undefined; }; - const searchParams = useSearchParams(); // 读取 query 的 thread_id(先用 hook,必要时用 window 兜底)。 const readQueryThreadId = () => { - const fromHook = searchParams.get("thread_id")?.trim(); + const fromHook = threadIdFromSearchParams; if (isValidThreadId(fromHook)) { return fromHook; } @@ -51,7 +55,7 @@ export function useThreadChat() { } return undefined; }; - + const queryThreadIdFromParams = readQueryThreadId(); // console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams); // 归一化:当值为 "new" 时,替换为 query 中的 thread_id(如果存在)。 @@ -65,20 +69,16 @@ export function useThreadChat() { [queryThreadIdFromParams], ); const intent = resolveThreadQueryIntent({ - pathThreadId: threadIdFromPath, + pathThreadId: threadIdFromPathOrParams, queryThreadId: queryThreadIdFromParams, isNewRoute, }); - const { isNewThread: isNewRequested, showWelcomeStyle, invalidNewRoute } = intent; - const effectiveThreadIdFromPath = - invalidNewRoute - ? undefined - : normalizeThreadId(threadIdFromPath) ?? - (isNewRoute ? undefined : readStoredThreadId()); + const { isNewThread: isNewRequested, showWelcomeStyle } = intent; + // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); - - const [threadId, setThreadId] = useState(() => { - return effectiveThreadIdFromPath ?? undefined; + + const [threadId, setThreadId] = useState(() => { + return threadIdFromPathOrParams; }); // New session is only controlled by `/workspace/chats/new`. @@ -91,17 +91,14 @@ export function useThreadChat() { } setIsNewThread(isNewRoute); // Prefer path thread id, fall back to query thread_id when path is /new. - setThreadId( - invalidNewRoute ? undefined : normalizeThreadId(threadIdFromPath), - ); + setThreadId(threadIdFromPathOrParams); }, [ - invalidNewRoute, isNewRoute, normalizeThreadId, pathname, searchParams, threadId, - threadIdFromPath, + threadIdFromPathOrParams, ]); const isMock = searchParams.get("mock") === "true"; return { @@ -110,7 +107,6 @@ export function useThreadChat() { setIsNewThread, isMock, showWelcomeStyle, - invalidNewRoute, }; } diff --git a/frontend/src/components/workspace/export-trigger.tsx b/frontend/src/components/workspace/export-trigger.tsx index db83e19b..c6b15d9b 100644 --- a/frontend/src/components/workspace/export-trigger.tsx +++ b/frontend/src/components/workspace/export-trigger.tsx @@ -21,7 +21,7 @@ import type { AgentThread } from "@/core/threads/types"; import { useThread } from "./messages/context"; import { Tooltip } from "./tooltip"; -export function ExportTrigger({ threadId }: { threadId?: string }) { +export function ExportTrigger({ threadId }: { threadId: string }) { const { t } = useI18n(); const { thread } = useThread(); diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index ecb32806..3b15c515 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -100,7 +100,7 @@ export function InputBox({ ...props }: Omit, "onSubmit"> & { assistantId?: string | null; - threadId?: string; + threadId: string; status?: ChatStatus; disabled?: boolean; context: Omit< diff --git a/frontend/src/components/workspace/messages/context.ts b/frontend/src/components/workspace/messages/context.ts index dede9ee8..3eea108b 100644 --- a/frontend/src/components/workspace/messages/context.ts +++ b/frontend/src/components/workspace/messages/context.ts @@ -5,7 +5,7 @@ import type { AgentThreadState } from "@/core/threads"; export interface ThreadContextType { thread: UseStream; - threadId?: string; + threadId: string; isMock?: boolean; } diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx index 55313d3e..bde754fa 100644 --- a/frontend/src/components/workspace/messages/message-list-item.tsx +++ b/frontend/src/components/workspace/messages/message-list-item.tsx @@ -45,7 +45,7 @@ export function MessageListItem({ className?: string; message: Message; isLoading?: boolean; - threadId?: string; + threadId: string; }) { const isHuman = message.type === "human"; return ( @@ -94,7 +94,7 @@ function MessageImage({ maxWidth = "90%", ...props }: React.ImgHTMLAttributes & { - threadId?: string; + threadId: string; maxWidth?: string; }) { if (!src) return null; @@ -124,7 +124,7 @@ function MessageContent_({ className?: string; message: Message; isLoading?: boolean; - threadId?: string; + threadId: string; }) { const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const isHuman = message.type === "human"; diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx index 1dc11543..2e8bc047 100644 --- a/frontend/src/components/workspace/messages/message-list.tsx +++ b/frontend/src/components/workspace/messages/message-list.tsx @@ -42,7 +42,7 @@ export function MessageList({ scrollButtonClassName, }: { className?: string; - threadId?: string; + threadId: string; thread: UseStream; /** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */ messagesOverride?: Message[]; diff --git a/frontend/src/components/workspace/thread-title.tsx b/frontend/src/components/workspace/thread-title.tsx index 35e9b57e..201b18f4 100644 --- a/frontend/src/components/workspace/thread-title.tsx +++ b/frontend/src/components/workspace/thread-title.tsx @@ -13,7 +13,7 @@ export function ThreadTitle({ threadTitle, }: { className?: string; - threadId?: string; + threadId: string; thread?: UseStream; threadTitle?: string; }) { diff --git a/frontend/src/core/artifacts/hooks.ts b/frontend/src/core/artifacts/hooks.ts index 6d875aba..78e411d3 100644 --- a/frontend/src/core/artifacts/hooks.ts +++ b/frontend/src/core/artifacts/hooks.ts @@ -11,7 +11,7 @@ export function useArtifactContent({ enabled, }: { filepath: string; - threadId?: string; + threadId: string; enabled?: boolean; }) { const isWriteFile = useMemo(() => { diff --git a/frontend/src/core/threads/hooks.test.ts b/frontend/src/core/threads/hooks.test.ts index 7967f87a..3e156191 100644 --- a/frontend/src/core/threads/hooks.test.ts +++ b/frontend/src/core/threads/hooks.test.ts @@ -26,5 +26,4 @@ void test("prefers path thread id over query thread id when not on /new", () => assert.equal(intent.isNewThread, false); assert.equal(intent.threadId, "thread-from-path"); - assert.equal(intent.invalidNewRoute, false); }); diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 9c0a368f..d8bc08d3 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -29,7 +29,7 @@ export type ToolEndEvent = { }; export type ThreadStreamOptions = { - threadId?: string | null | undefined; + threadId: string | null | undefined; context: LocalSettings["context"]; createNewSession?: boolean; isMock?: boolean; diff --git a/frontend/src/core/threads/utils.ts b/frontend/src/core/threads/utils.ts index 8d8db93f..651a1420 100644 --- a/frontend/src/core/threads/utils.ts +++ b/frontend/src/core/threads/utils.ts @@ -12,7 +12,6 @@ export interface ThreadQueryIntent { threadId: string | undefined; isNewThread: boolean; showWelcomeStyle: boolean; - invalidNewRoute: boolean; } export function pathOfThread(threadId: string) { @@ -45,8 +44,6 @@ export function resolveThreadQueryIntent({ // 新逻辑只由路由 /workspace/chats/new 控制“新会话” isNewThread, showWelcomeStyle: isNewThread, - // 新逻辑下不再要求 /new 必带 query thread_id - invalidNewRoute: false, }; } diff --git a/frontend/src/hooks/use-selected-skill-listener.ts b/frontend/src/hooks/use-selected-skill-listener.ts index bba41577..7fd6721e 100644 --- a/frontend/src/hooks/use-selected-skill-listener.ts +++ b/frontend/src/hooks/use-selected-skill-listener.ts @@ -19,7 +19,7 @@ interface SkillError { interface UseSelectedSkillListenerOptions { /** 当前会话 thread_id,用于调用 bootstrapRemoteSkill */ - threadId?: string | null; + threadId: string | null; } interface UseSelectedSkillListenerReturn { diff --git a/frontend/tests/e2e/support/chat-helpers.ts b/frontend/tests/e2e/support/chat-helpers.ts index 0eaf1c25..a3b3501a 100644 --- a/frontend/tests/e2e/support/chat-helpers.ts +++ b/frontend/tests/e2e/support/chat-helpers.ts @@ -34,7 +34,7 @@ export function buildChatUrl({ }: { pathThreadId?: string; xclawUsed: boolean; - threadId?: string; + threadId: string; }) { const resolvedThreadId = threadId ?? pathThreadId; if (!pathThreadId && !resolvedThreadId) {