fix: 收紧threadId的类型为string,删除无用isNewRoute变量,合并状态判断showInputBox至showWelcomeStyle

This commit is contained in:
肖应宇 2026-04-07 16:08:42 +08:00
parent 7012693802
commit 1606d79bcb
16 changed files with 50 additions and 83 deletions

View File

@ -61,7 +61,6 @@ export default function ChatPage() {
setIsNewThread, setIsNewThread,
isMock, isMock,
showWelcomeStyle, showWelcomeStyle,
invalidNewRoute,
} = useThreadChat(); } = useThreadChat();
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/xclaw_used 参数。 // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/xclaw_used 参数。
@ -119,7 +118,6 @@ export default function ChatPage() {
}, [thread.values?.title]); }, [thread.values?.title]);
const [hasSubmitted, setHasSubmitted] = useState(false); const [hasSubmitted, setHasSubmitted] = useState(false);
const showInputBox = !invalidNewRoute && !(showWelcomeStyle && thread.isThreadLoading);
const [historyCutoff, setHistoryCutoff] = useState<number | null>(null); const [historyCutoff, setHistoryCutoff] = useState<number | null>(null);
useEffect(() => { useEffect(() => {
@ -214,12 +212,10 @@ export default function ChatPage() {
setArtifactsOpen, setArtifactsOpen,
setIsNewThread, setIsNewThread,
]); ]);
// shouldRenderHistory || historyCutoff === null
// console.log('shouldRenderHistory', shouldRenderHistory, 'historyCutoff', historyCutoff);
return ( return (
<ThreadContext.Provider value={{ thread }}> <ThreadContext.Provider value={{ threadId,thread }}>
<div <div
className={cn( className={cn(
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out", "m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
@ -313,43 +309,22 @@ export default function ChatPage() {
)} )}
> >
<div className="flex size-full justify-center"> <div className="flex size-full justify-center">
{invalidNewRoute ? ( <MessageList
<div className="flex size-full items-center justify-center px-6"> className={cn(
<div "size-full",
className="max-w-md rounded-2xl border border-[#E5DDF2] bg-white px-6 py-5 text-center shadow-sm" (!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
data-testid="missing-thread-id-state" )}
role="alert" threadId={threadId}
> thread={thread}
<h2 className="text-base font-semibold text-[#150033]"> messagesOverride={
thread_id shouldRenderHistory || historyCutoff === null
</h2> ? undefined
<p className="mt-2 text-sm text-[#666666]"> : thread.messages.slice(historyCutoff)
访 }
<span className="mx-1 font-mono">/workspace/chats/new</span> paddingBottom={todoListCollapsed ? 160 : 280}
showScrollToBottomButton={!showWelcomeStyle}
<span className="mx-1 font-mono">?thread_id=...</span> scrollButtonClassName="bottom-[112px]"
使 />
</p>
</div>
</div>
) : (
<MessageList
className={cn(
"size-full",
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
)}
threadId={threadId}
thread={thread}
messagesOverride={
shouldRenderHistory || historyCutoff === null
? undefined
: thread.messages.slice(historyCutoff)
}
paddingBottom={todoListCollapsed ? 160 : 280}
showScrollToBottomButton={!showWelcomeStyle}
scrollButtonClassName="bottom-[112px]"
/>
)}
</div> </div>
</main> </main>
</div> </div>
@ -433,7 +408,7 @@ export default function ChatPage() {
showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]", showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
)} )}
> >
{showInputBox ? ( {!(showWelcomeStyle && thread.isThreadLoading) ? (
<InputBox <InputBox
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")} className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
threadId={threadId} threadId={threadId}

View File

@ -61,7 +61,7 @@ export function ArtifactFileDetail({
}: { }: {
className?: string; className?: string;
filepath: string; filepath: string;
threadId?: string; threadId: string;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const { artifacts, setOpen, select, fullscreen, setFullscreen } = const { artifacts, setOpen, select, fullscreen, setFullscreen } =
@ -511,7 +511,7 @@ export function ArtifactFilePreview({
content: string; content: string;
language: string; language: string;
zoom?: number; zoom?: number;
threadId?: string; threadId: string;
}) { }) {
const zoomScale = zoom / 100; const zoomScale = zoom / 100;
const normalizedContent = useMemo(() => { const normalizedContent = useMemo(() => {

View File

@ -29,7 +29,7 @@ export function ArtifactFileList({
}: { }: {
className?: string; className?: string;
files: string[]; files: string[];
threadId?: string; threadId: string;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const { select: selectArtifact, setOpen } = useArtifacts(); const { select: selectArtifact, setOpen } = useArtifacts();

View File

@ -7,7 +7,9 @@ import { resolveThreadQueryIntent } from "@/core/threads/utils";
export function useThreadChat() { export function useThreadChat() {
const pathname = usePathname(); 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。 // 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。
const threadIdFromPathname = (() => { const threadIdFromPathname = (() => {
const parts = pathname.split("?")[0]?.split("/") ?? []; const parts = pathname.split("?")[0]?.split("/") ?? [];
@ -18,8 +20,11 @@ export function useThreadChat() {
return undefined; return undefined;
})(); })();
const rawPathThreadId = params?.thread_id ?? threadIdFromPathname; const rawPathThreadId = params?.thread_id ?? threadIdFromPathname;
const isNewRoute = rawPathThreadId === "new"; 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] pathname", pathname);
// console.log("[useThreadChat] params.thread_id", params?.thread_id); // console.log("[useThreadChat] params.thread_id", params?.thread_id);
// console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname); // console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname);
@ -33,10 +38,9 @@ export function useThreadChat() {
return isValidThreadId(stored) ? stored : undefined; return isValidThreadId(stored) ? stored : undefined;
}; };
const searchParams = useSearchParams();
// 读取 query 的 thread_id先用 hook必要时用 window 兜底)。 // 读取 query 的 thread_id先用 hook必要时用 window 兜底)。
const readQueryThreadId = () => { const readQueryThreadId = () => {
const fromHook = searchParams.get("thread_id")?.trim(); const fromHook = threadIdFromSearchParams;
if (isValidThreadId(fromHook)) { if (isValidThreadId(fromHook)) {
return fromHook; return fromHook;
} }
@ -51,7 +55,7 @@ export function useThreadChat() {
} }
return undefined; return undefined;
}; };
const queryThreadIdFromParams = readQueryThreadId(); const queryThreadIdFromParams = readQueryThreadId();
// console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams); // console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams);
// 归一化:当值为 "new" 时,替换为 query 中的 thread_id如果存在 // 归一化:当值为 "new" 时,替换为 query 中的 thread_id如果存在
@ -65,20 +69,16 @@ export function useThreadChat() {
[queryThreadIdFromParams], [queryThreadIdFromParams],
); );
const intent = resolveThreadQueryIntent({ const intent = resolveThreadQueryIntent({
pathThreadId: threadIdFromPath, pathThreadId: threadIdFromPathOrParams,
queryThreadId: queryThreadIdFromParams, queryThreadId: queryThreadIdFromParams,
isNewRoute, isNewRoute,
}); });
const { isNewThread: isNewRequested, showWelcomeStyle, invalidNewRoute } = intent; const { isNewThread: isNewRequested, showWelcomeStyle } = intent;
const effectiveThreadIdFromPath =
invalidNewRoute
? undefined
: normalizeThreadId(threadIdFromPath) ??
(isNewRoute ? undefined : readStoredThreadId());
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
const [threadId, setThreadId] = useState(() => { const [threadId, setThreadId] = useState<string>(() => {
return effectiveThreadIdFromPath ?? undefined; return threadIdFromPathOrParams;
}); });
// New session is only controlled by `/workspace/chats/new`. // New session is only controlled by `/workspace/chats/new`.
@ -91,17 +91,14 @@ export function useThreadChat() {
} }
setIsNewThread(isNewRoute); setIsNewThread(isNewRoute);
// Prefer path thread id, fall back to query thread_id when path is /new. // Prefer path thread id, fall back to query thread_id when path is /new.
setThreadId( setThreadId(threadIdFromPathOrParams);
invalidNewRoute ? undefined : normalizeThreadId(threadIdFromPath),
);
}, [ }, [
invalidNewRoute,
isNewRoute, isNewRoute,
normalizeThreadId, normalizeThreadId,
pathname, pathname,
searchParams, searchParams,
threadId, threadId,
threadIdFromPath, threadIdFromPathOrParams,
]); ]);
const isMock = searchParams.get("mock") === "true"; const isMock = searchParams.get("mock") === "true";
return { return {
@ -110,7 +107,6 @@ export function useThreadChat() {
setIsNewThread, setIsNewThread,
isMock, isMock,
showWelcomeStyle, showWelcomeStyle,
invalidNewRoute,
}; };
} }

View File

@ -21,7 +21,7 @@ import type { AgentThread } from "@/core/threads/types";
import { useThread } from "./messages/context"; import { useThread } from "./messages/context";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
export function ExportTrigger({ threadId }: { threadId?: string }) { export function ExportTrigger({ threadId }: { threadId: string }) {
const { t } = useI18n(); const { t } = useI18n();
const { thread } = useThread(); const { thread } = useThread();

View File

@ -100,7 +100,7 @@ export function InputBox({
...props ...props
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & { }: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null; assistantId?: string | null;
threadId?: string; threadId: string;
status?: ChatStatus; status?: ChatStatus;
disabled?: boolean; disabled?: boolean;
context: Omit< context: Omit<

View File

@ -5,7 +5,7 @@ import type { AgentThreadState } from "@/core/threads";
export interface ThreadContextType { export interface ThreadContextType {
thread: UseStream<AgentThreadState>; thread: UseStream<AgentThreadState>;
threadId?: string; threadId: string;
isMock?: boolean; isMock?: boolean;
} }

View File

@ -45,7 +45,7 @@ export function MessageListItem({
className?: string; className?: string;
message: Message; message: Message;
isLoading?: boolean; isLoading?: boolean;
threadId?: string; threadId: string;
}) { }) {
const isHuman = message.type === "human"; const isHuman = message.type === "human";
return ( return (
@ -94,7 +94,7 @@ function MessageImage({
maxWidth = "90%", maxWidth = "90%",
...props ...props
}: React.ImgHTMLAttributes<HTMLImageElement> & { }: React.ImgHTMLAttributes<HTMLImageElement> & {
threadId?: string; threadId: string;
maxWidth?: string; maxWidth?: string;
}) { }) {
if (!src) return null; if (!src) return null;
@ -124,7 +124,7 @@ function MessageContent_({
className?: string; className?: string;
message: Message; message: Message;
isLoading?: boolean; isLoading?: boolean;
threadId?: string; threadId: string;
}) { }) {
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
const isHuman = message.type === "human"; const isHuman = message.type === "human";

View File

@ -42,7 +42,7 @@ export function MessageList({
scrollButtonClassName, scrollButtonClassName,
}: { }: {
className?: string; className?: string;
threadId?: string; threadId: string;
thread: UseStream<AgentThreadState>; thread: UseStream<AgentThreadState>;
/** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */ /** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */
messagesOverride?: Message[]; messagesOverride?: Message[];

View File

@ -13,7 +13,7 @@ export function ThreadTitle({
threadTitle, threadTitle,
}: { }: {
className?: string; className?: string;
threadId?: string; threadId: string;
thread?: UseStream<AgentThreadState>; thread?: UseStream<AgentThreadState>;
threadTitle?: string; threadTitle?: string;
}) { }) {

View File

@ -11,7 +11,7 @@ export function useArtifactContent({
enabled, enabled,
}: { }: {
filepath: string; filepath: string;
threadId?: string; threadId: string;
enabled?: boolean; enabled?: boolean;
}) { }) {
const isWriteFile = useMemo(() => { const isWriteFile = useMemo(() => {

View File

@ -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.isNewThread, false);
assert.equal(intent.threadId, "thread-from-path"); assert.equal(intent.threadId, "thread-from-path");
assert.equal(intent.invalidNewRoute, false);
}); });

View File

@ -29,7 +29,7 @@ export type ToolEndEvent = {
}; };
export type ThreadStreamOptions = { export type ThreadStreamOptions = {
threadId?: string | null | undefined; threadId: string | null | undefined;
context: LocalSettings["context"]; context: LocalSettings["context"];
createNewSession?: boolean; createNewSession?: boolean;
isMock?: boolean; isMock?: boolean;

View File

@ -12,7 +12,6 @@ export interface ThreadQueryIntent {
threadId: string | undefined; threadId: string | undefined;
isNewThread: boolean; isNewThread: boolean;
showWelcomeStyle: boolean; showWelcomeStyle: boolean;
invalidNewRoute: boolean;
} }
export function pathOfThread(threadId: string) { export function pathOfThread(threadId: string) {
@ -45,8 +44,6 @@ export function resolveThreadQueryIntent({
// 新逻辑只由路由 /workspace/chats/new 控制“新会话” // 新逻辑只由路由 /workspace/chats/new 控制“新会话”
isNewThread, isNewThread,
showWelcomeStyle: isNewThread, showWelcomeStyle: isNewThread,
// 新逻辑下不再要求 /new 必带 query thread_id
invalidNewRoute: false,
}; };
} }

View File

@ -19,7 +19,7 @@ interface SkillError {
interface UseSelectedSkillListenerOptions { interface UseSelectedSkillListenerOptions {
/** 当前会话 thread_id用于调用 bootstrapRemoteSkill */ /** 当前会话 thread_id用于调用 bootstrapRemoteSkill */
threadId?: string | null; threadId: string | null;
} }
interface UseSelectedSkillListenerReturn { interface UseSelectedSkillListenerReturn {

View File

@ -34,7 +34,7 @@ export function buildChatUrl({
}: { }: {
pathThreadId?: string; pathThreadId?: string;
xclawUsed: boolean; xclawUsed: boolean;
threadId?: string; threadId: string;
}) { }) {
const resolvedThreadId = threadId ?? pathThreadId; const resolvedThreadId = threadId ?? pathThreadId;
if (!pathThreadId && !resolvedThreadId) { if (!pathThreadId && !resolvedThreadId) {