fix: ghost session cleanup, new chat switching, and markdown overflow handling (#287)

This commit is contained in:
DigHuang
2026-03-04 18:54:47 +08:00
committed by GitHub
Unverified
parent 89028756e1
commit 76df84e68c
3 changed files with 37 additions and 7 deletions

View File

@@ -346,9 +346,9 @@ function MessageBubble({
)} )}
> >
{isUser ? ( {isUser ? (
<p className="whitespace-pre-wrap break-words text-sm">{text}</p> <p className="whitespace-pre-wrap break-words break-all text-sm">{text}</p>
) : ( ) : (
<div className="prose prose-sm dark:prose-invert max-w-none"> <div className="prose prose-sm dark:prose-invert max-w-none break-words break-all">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
@@ -357,7 +357,7 @@ function MessageBubble({
const isInline = !match && !className; const isInline = !match && !className;
if (isInline) { if (isInline) {
return ( return (
<code className="bg-background/50 px-1.5 py-0.5 rounded text-sm font-mono" {...props}> <code className="bg-background/50 px-1.5 py-0.5 rounded text-sm font-mono break-words break-all" {...props}>
{children} {children}
</code> </code>
); );
@@ -372,7 +372,7 @@ function MessageBubble({
}, },
a({ href, children }) { a({ href, children }) {
return ( return (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline"> <a href={href} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-words break-all">
{children} {children}
</a> </a>
); );

View File

@@ -35,6 +35,8 @@ export function Chat() {
const abortRun = useChatStore((s) => s.abortRun); const abortRun = useChatStore((s) => s.abortRun);
const clearError = useChatStore((s) => s.clearError); const clearError = useChatStore((s) => s.clearError);
const cleanupEmptySession = useChatStore((s) => s.cleanupEmptySession);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [streamingTimestamp, setStreamingTimestamp] = useState<number>(0); const [streamingTimestamp, setStreamingTimestamp] = useState<number>(0);
@@ -54,8 +56,11 @@ export function Chat() {
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
// If the user navigates away without sending any messages, remove the
// empty session so it doesn't linger as a ghost entry in the sidebar.
cleanupEmptySession();
}; };
}, [isGatewayRunning, loadHistory, loadSessions]); }, [isGatewayRunning, loadHistory, loadSessions, cleanupEmptySession]);
// Auto-scroll on new messages, streaming, or activity changes // Auto-scroll on new messages, streaming, or activity changes
useEffect(() => { useEffect(() => {

View File

@@ -99,6 +99,7 @@ interface ChatState {
switchSession: (key: string) => void; switchSession: (key: string) => void;
newSession: () => void; newSession: () => void;
deleteSession: (key: string) => Promise<void>; deleteSession: (key: string) => Promise<void>;
cleanupEmptySession: () => void;
loadHistory: (quiet?: boolean) => Promise<void>; loadHistory: (quiet?: boolean) => Promise<void>;
sendMessage: (text: string, attachments?: Array<{ fileName: string; mimeType: string; fileSize: number; stagedPath: string; preview: string | null }>) => Promise<void>; sendMessage: (text: string, attachments?: Array<{ fileName: string; mimeType: string; fileSize: number; stagedPath: string; preview: string | null }>) => Promise<void>;
abortRun: () => Promise<void>; abortRun: () => Promise<void>;
@@ -973,8 +974,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
} }
} }
if (!dedupedSessions.find((s) => s.key === nextSessionKey) && dedupedSessions.length > 0) { if (!dedupedSessions.find((s) => s.key === nextSessionKey) && dedupedSessions.length > 0) {
// Current session not found at all — switch to the first available session // Current session not found in the backend list
nextSessionKey = dedupedSessions[0].key; const isNewEmptySession = get().messages.length === 0;
if (!isNewEmptySession) {
nextSessionKey = dedupedSessions[0].key;
}
} }
const sessionsWithCurrent = !dedupedSessions.find((s) => s.key === nextSessionKey) && nextSessionKey const sessionsWithCurrent = !dedupedSessions.find((s) => s.key === nextSessionKey) && nextSessionKey
@@ -1153,6 +1157,27 @@ export const useChatStore = create<ChatState>((set, get) => ({
})); }));
}, },
// ── Cleanup empty session on navigate away ──
cleanupEmptySession: () => {
const { currentSessionKey, messages } = get();
// Only remove non-main sessions that were never used (no messages sent).
// This mirrors the "leavingEmpty" logic in switchSession so that creating
// a new session and immediately navigating away doesn't leave a ghost entry
// in the sidebar.
const isEmptyNonMain = !currentSessionKey.endsWith(':main') && messages.length === 0;
if (!isEmptyNonMain) return;
set((s) => ({
sessions: s.sessions.filter((sess) => sess.key !== currentSessionKey),
sessionLabels: Object.fromEntries(
Object.entries(s.sessionLabels).filter(([k]) => k !== currentSessionKey),
),
sessionLastActivity: Object.fromEntries(
Object.entries(s.sessionLastActivity).filter(([k]) => k !== currentSessionKey),
),
}));
},
// ── Load chat history ── // ── Load chat history ──
loadHistory: async (quiet = false) => { loadHistory: async (quiet = false) => {