fix(webui): default to new chat on load and preserve scroll on settings return

- Remove auto-selection of the most recent session on initial load,
  so the app opens to a blank new-chat page instead of the last session.
- Preserve active session state when navigating to/from settings:
  keep ThreadShell mounted (hidden via CSS) so scroll position, message
  cache, and streaming state are not lost.
- Update onBackToChat to return to blank page when no session was active
  instead of falling back to the most recent session.
- Update related test expectations to match the new navigation behavior.
This commit is contained in:
彭星杰 2026-05-12 22:27:22 +08:00 committed by Xubin Ren
parent 9e15925cf4
commit 00597fccd6
2 changed files with 26 additions and 28 deletions

View File

@ -250,7 +250,6 @@ function Shell({ onModelNameChange, onLogout }: { onModelNameChange: (modelName:
key: string;
label: string;
} | null>(null);
const lastSessionsLen = useRef(0);
const restartSawDisconnectRef = useRef(false);
const [restartToast, setRestartToast] = useState<string | null>(null);
const [isRestarting, setIsRestarting] = useState(false);
@ -266,13 +265,7 @@ function Shell({ onModelNameChange, onLogout }: { onModelNameChange: (modelName:
}
}, [desktopSidebarOpen]);
useEffect(() => {
if (activeKey) return;
if (sessions.length > 0 && lastSessionsLen.current === 0) {
setActiveKey(sessions[0].key);
}
lastSessionsLen.current = sessions.length;
}, [sessions, activeKey]);
const activeSession = useMemo<ChatSummary | null>(() => {
if (!activeKey) return null;
@ -335,9 +328,8 @@ function Shell({ onModelNameChange, onLogout }: { onModelNameChange: (modelName:
setView("chat");
setMobileSidebarOpen(false);
setActiveKey((current) => {
if (current && sessions.some((session) => session.key === current)) {
return current;
}
if (!current) return null;
if (sessions.some((session) => session.key === current)) return current;
return sessions[0]?.key ?? null;
});
}, [sessions]);
@ -479,18 +471,13 @@ function Shell({ onModelNameChange, onLogout }: { onModelNameChange: (modelName:
</Sheet>
) : null}
<main className="flex h-full min-w-0 flex-1 flex-col">
{view === "settings" ? (
<SettingsView
theme={theme}
onToggleTheme={toggle}
onBackToChat={onBackToChat}
onModelNameChange={onModelNameChange}
onLogout={onLogout}
onRestart={onRestart}
isRestarting={isRestarting}
/>
) : (
<main className="relative flex h-full min-w-0 flex-1 flex-col">
<div
className={cn(
"absolute inset-0 flex flex-col",
view === "settings" && "invisible pointer-events-none",
)}
>
<ThreadShell
session={activeSession}
title={headerTitle}
@ -502,6 +489,19 @@ function Shell({ onModelNameChange, onLogout }: { onModelNameChange: (modelName:
onToggleTheme={toggle}
hideSidebarToggleOnDesktop={desktopSidebarOpen}
/>
</div>
{view === "settings" && (
<div className="absolute inset-0 flex flex-col">
<SettingsView
theme={theme}
onToggleTheme={toggle}
onBackToChat={onBackToChat}
onModelNameChange={onModelNameChange}
onLogout={onLogout}
onRestart={onRestart}
isRestarting={isRestarting}
/>
</div>
)}
</main>

View File

@ -265,7 +265,7 @@ describe("App layout", () => {
expect(screen.queryByDisplayValue("unsaved-brave-key")).not.toBeInTheDocument();
});
it("returns from settings to an available chat instead of the blank start page", async () => {
it("returns from settings to the blank start page when no session was active", async () => {
mockSessions = [
{
key: "websocket:chat-a",
@ -330,10 +330,8 @@ describe("App layout", () => {
expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Back to chat" }));
await waitFor(() => expect(document.title).toBe("First chat · nanobot"));
const restoredSidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
fireEvent.click(within(restoredSidebar).getByRole("button", { name: /^Second chat$/ }));
await waitFor(() => expect(document.title).toBe("Second chat · nanobot"));
await waitFor(() => expect(document.title).toBe("nanobot"));
expect(screen.getByText("What can I do for you?")).toBeInTheDocument();
});
it("filters sidebar sessions through the lightweight search row", async () => {