mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-26 13:35:49 +08:00
Merge branch 'main' into feat/voice
This commit is contained in:
@@ -2,6 +2,8 @@ import { useEffect, useRef } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
|
||||
import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
|
||||
import { showCompletionTitleIndicator } from '../../../utils/pageTitleNotification';
|
||||
import { playChatCompletionSound } from '../../../utils/notificationSound';
|
||||
import type { PendingPermissionRequest, SessionNavigationOptions } from '../types/types';
|
||||
import type { ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
@@ -285,6 +287,9 @@ export function useChatRealtimeHandlers({
|
||||
break;
|
||||
}
|
||||
|
||||
showCompletionTitleIndicator();
|
||||
void playChatCompletionSound();
|
||||
|
||||
const actualSessionId =
|
||||
typeof msg.actualSessionId === 'string' && msg.actualSessionId.trim().length > 0
|
||||
? msg.actualSessionId
|
||||
|
||||
@@ -383,12 +383,47 @@ export function useChatSessionState({
|
||||
setIsUserScrolledUp(false);
|
||||
}, [selectedProject?.projectId, selectedSession?.id]);
|
||||
|
||||
// Initial scroll to bottom
|
||||
// Initial scroll to bottom — robust to lazy content reflow.
|
||||
// The previous implementation fired one scrollToBottom() at +200ms and
|
||||
// cleared the pending flag. When markdown blocks, code highlighting, or
|
||||
// images finished rendering after that window, scrollHeight grew but
|
||||
// nothing re-anchored the viewport, leaving the chat tab visually
|
||||
// "scrolled way up" with the latest assistant message off-screen.
|
||||
//
|
||||
// This version re-scrolls every animation frame while scrollHeight is
|
||||
// still growing, capped at ~1s (60 frames) or 3 consecutive stable
|
||||
// frames. Cancels cleanly on session change via the pending flag.
|
||||
useEffect(() => {
|
||||
if (!pendingInitialScrollRef.current || !scrollContainerRef.current || isLoadingSessionMessages) return;
|
||||
if (chatMessages.length === 0) { pendingInitialScrollRef.current = false; return; }
|
||||
pendingInitialScrollRef.current = false;
|
||||
if (!searchScrollActiveRef.current) setTimeout(() => scrollToBottom(), 200);
|
||||
if (searchScrollActiveRef.current) { pendingInitialScrollRef.current = false; return; }
|
||||
|
||||
const container = scrollContainerRef.current;
|
||||
let frame = 0;
|
||||
let lastHeight = 0;
|
||||
let stableCount = 0;
|
||||
let rafId = 0;
|
||||
|
||||
const tick = () => {
|
||||
if (!pendingInitialScrollRef.current || !scrollContainerRef.current) return;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
if (container.scrollHeight === lastHeight) {
|
||||
stableCount++;
|
||||
} else {
|
||||
stableCount = 0;
|
||||
lastHeight = container.scrollHeight;
|
||||
}
|
||||
frame++;
|
||||
if (stableCount < 3 && frame < 60) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
} else {
|
||||
pendingInitialScrollRef.current = false;
|
||||
}
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [chatMessages.length, isLoadingSessionMessages, scrollToBottom]);
|
||||
|
||||
// Main session loading effect — store-based
|
||||
|
||||
@@ -393,7 +393,8 @@ export function useSlashCommands({
|
||||
return;
|
||||
}
|
||||
|
||||
const slashPattern = /^\/(\S*)$/;
|
||||
// Match / at start of input OR after whitespace, capturing the /word up to cursor.
|
||||
const slashPattern = /(?:^|\s)(\/\S*)$/;
|
||||
const match = textBeforeCursor.match(slashPattern);
|
||||
|
||||
if (!match) {
|
||||
@@ -401,8 +402,9 @@ export function useSlashCommands({
|
||||
return;
|
||||
}
|
||||
|
||||
const slashPos = 0;
|
||||
const query = match[1];
|
||||
// Compute actual position of / in the full input string.
|
||||
const slashPos = match.index! + (match[0].length - match[1].length);
|
||||
const query = match[1].slice(1); // strip leading /
|
||||
|
||||
setSlashPosition(slashPos);
|
||||
setShowCommandMenu(true);
|
||||
|
||||
Reference in New Issue
Block a user