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

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

View File

@ -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<number | null>(null);
useEffect(() => {
@ -214,12 +212,10 @@ export default function ChatPage() {
setArtifactsOpen,
setIsNewThread,
]);
// shouldRenderHistory || historyCutoff === null
// console.log('shouldRenderHistory', shouldRenderHistory, 'historyCutoff', historyCutoff);
return (
<ThreadContext.Provider value={{ thread }}>
<ThreadContext.Provider value={{ threadId,thread }}>
<div
className={cn(
"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">
{invalidNewRoute ? (
<div className="flex size-full items-center justify-center px-6">
<div
className="max-w-md rounded-2xl border border-[#E5DDF2] bg-white px-6 py-5 text-center shadow-sm"
data-testid="missing-thread-id-state"
role="alert"
>
<h2 className="text-base font-semibold text-[#150033]">
thread_id
</h2>
<p className="mt-2 text-sm text-[#666666]">
访
<span className="mx-1 font-mono">/workspace/chats/new</span>
<span className="mx-1 font-mono">?thread_id=...</span>
使
</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]"
/>
)}
<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>
</main>
</div>
@ -433,7 +408,7 @@ export default function ChatPage() {
showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
)}
>
{showInputBox ? (
{!(showWelcomeStyle && thread.isThreadLoading) ? (
<InputBox
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
threadId={threadId}

View File

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

View File

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

View File

@ -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<string>(() => {
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,
};
}

View File

@ -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();

View File

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

View File

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

View File

@ -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<HTMLImageElement> & {
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";

View File

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

View File

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

View File

@ -11,7 +11,7 @@ export function useArtifactContent({
enabled,
}: {
filepath: string;
threadId?: string;
threadId: string;
enabled?: boolean;
}) {
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.threadId, "thread-from-path");
assert.equal(intent.invalidNewRoute, false);
});

View File

@ -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;

View File

@ -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,
};
}

View File

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

View File

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