feat(ui): add dynamic window title and sprite-based project/file icons (#529)
* feat(ui): add dynamic titles and sprite-based project/file icons * feat(files): add viewer syntax fallback and tab file icons * fix(files): restore file viewer highlighting and add diff file icons * feat(git): add file icons and async file-viewer syntax fallback * fix(files): force codemirror token colors in file viewer * feat(files): add shiki view mode for file viewer * fix(files): force codemirror parse after programmatic content updates * feat(files): support markdown frontmatter preview * feat(chat): use pierre diffs for tool previews * feat(chat): add configurable beautiful-mermaid rendering * feat(perf): virtualize chat rendering and add react-scan toggle * feat(build): enable React Compiler in Vite React apps * fix(chat): reduce rerenders from tooltips and streamed activity * fix(ui): make MessageList React Compiler safe * chore(ui): batch commit remaining pending ui updates * fix: polish chat and diff preview rendering - Keep Mermaid action buttons fixed while diagram content scrolls - Align Diff All Files headers and match Git-style path truncation - Default chat tool diffs to unified view with lightweight indicators disabled * fix: preserve file tree expansion and delay git action label collapse * fix: refine project icon controls in settings --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d6b8f28e6f
commit
1d8ff97c95
@@ -8,7 +8,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import ChatEmptyState from './ChatEmptyState';
|
||||
import MessageList from './MessageList';
|
||||
import MessageList, { type MessageListHandle } from './MessageList';
|
||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
import { useChatScrollManager } from '@/hooks/useChatScrollManager';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
@@ -166,6 +166,7 @@ export const ChatContainer: React.FC = () => {
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const draftOpen = Boolean(newSessionDraft?.open);
|
||||
const isDesktopExpandedInput = isExpandedInput && !isMobile;
|
||||
const messageListRef = React.useRef<MessageListHandle | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentSessionId && !draftOpen) {
|
||||
@@ -176,6 +177,7 @@ export const ChatContainer: React.FC = () => {
|
||||
const [turnStart, setTurnStart] = React.useState(0);
|
||||
const turnHandleRef = React.useRef<number | null>(null);
|
||||
const turnIdleRef = React.useRef(false);
|
||||
const initializedTurnStartSessionRef = React.useRef<string | null>(null);
|
||||
const TURN_INIT = 5;
|
||||
const TURN_BATCH = 8;
|
||||
|
||||
@@ -286,6 +288,16 @@ export const ChatContainer: React.FC = () => {
|
||||
React.useEffect(() => {
|
||||
cancelTurnBackfill();
|
||||
if (!currentSessionId) {
|
||||
initializedTurnStartSessionRef.current = null;
|
||||
setTurnStart(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (initializedTurnStartSessionRef.current === currentSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionMessages.length === 0) {
|
||||
setTurnStart(0);
|
||||
return;
|
||||
}
|
||||
@@ -293,14 +305,21 @@ export const ChatContainer: React.FC = () => {
|
||||
const turnCount = userTurnIndexes.length;
|
||||
const start = turnCount > TURN_INIT ? turnCount - TURN_INIT : 0;
|
||||
setTurnStart(start);
|
||||
}, [cancelTurnBackfill, currentSessionId, userTurnIndexes.length]);
|
||||
initializedTurnStartSessionRef.current = currentSessionId;
|
||||
}, [cancelTurnBackfill, currentSessionId, sessionMessages.length, userTurnIndexes.length]);
|
||||
|
||||
const isSessionActive = sessionStatusForCurrent.type === 'busy' || sessionStatusForCurrent.type === 'retry';
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isSessionActive) {
|
||||
cancelTurnBackfill();
|
||||
return;
|
||||
}
|
||||
scheduleTurnBackfill();
|
||||
return () => {
|
||||
cancelTurnBackfill();
|
||||
};
|
||||
}, [cancelTurnBackfill, scheduleTurnBackfill, turnStart]);
|
||||
}, [cancelTurnBackfill, isSessionActive, scheduleTurnBackfill, turnStart]);
|
||||
|
||||
const hasMoreAbove = React.useMemo(() => {
|
||||
if (!memoryState) {
|
||||
@@ -344,19 +363,22 @@ export const ChatContainer: React.FC = () => {
|
||||
setTurnStart(0);
|
||||
|
||||
const container = scrollRef.current;
|
||||
const anchor = messageListRef.current?.captureViewportAnchor() ?? null;
|
||||
const prevHeight = container?.scrollHeight ?? null;
|
||||
const prevTop = container?.scrollTop ?? null;
|
||||
|
||||
setIsLoadingOlder(true);
|
||||
try {
|
||||
await loadMoreMessages(currentSessionId, 'up');
|
||||
if (container && prevHeight !== null && prevTop !== null) {
|
||||
const heightDiff = container.scrollHeight - prevHeight;
|
||||
scrollToPosition(prevTop + heightDiff, { instant: true });
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingOlder(false);
|
||||
}
|
||||
void loadMoreMessages(currentSessionId, 'up')
|
||||
.then(() => {
|
||||
const restored = anchor ? (messageListRef.current?.restoreViewportAnchor(anchor) ?? false) : false;
|
||||
if (!restored && container && prevHeight !== null && prevTop !== null) {
|
||||
const heightDiff = container.scrollHeight - prevHeight;
|
||||
scrollToPosition(prevTop + heightDiff, { instant: true });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingOlder(false);
|
||||
});
|
||||
}, [cancelTurnBackfill, currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef, scrollToPosition]);
|
||||
|
||||
const handleRenderEarlier = React.useCallback(() => {
|
||||
@@ -366,6 +388,10 @@ export const ChatContainer: React.FC = () => {
|
||||
|
||||
// Scroll to a specific message by ID (for timeline dialog)
|
||||
const scrollToMessage = React.useCallback((messageId: string) => {
|
||||
if (messageListRef.current?.scrollToMessageId(messageId, { behavior: 'smooth' })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
@@ -396,12 +422,9 @@ export const ChatContainer: React.FC = () => {
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
await loadMessages(currentSessionId);
|
||||
} finally {
|
||||
await loadMessages(currentSessionId).finally(() => {
|
||||
const statusType = sessionStatusForCurrent.type ?? 'idle';
|
||||
const isActivePhase = statusType === 'busy' || statusType === 'retry';
|
||||
// When pinned and active, scroll is already maintained automatically
|
||||
const shouldSkipScroll = isActivePhase && isPinned;
|
||||
|
||||
if (!shouldSkipScroll) {
|
||||
@@ -413,7 +436,7 @@ export const ChatContainer: React.FC = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
void load();
|
||||
@@ -536,6 +559,7 @@ export const ChatContainer: React.FC = () => {
|
||||
>
|
||||
<div className="relative z-0 min-h-full">
|
||||
<MessageList
|
||||
ref={messageListRef}
|
||||
messages={renderedSessionMessages}
|
||||
permissions={sessionPermissions}
|
||||
questions={sessionQuestions}
|
||||
@@ -547,6 +571,7 @@ export const ChatContainer: React.FC = () => {
|
||||
hasRenderEarlier={turnStart > 0}
|
||||
onRenderEarlier={handleRenderEarlier}
|
||||
scrollToBottom={scrollToBottom}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
|
||||
Reference in New Issue
Block a user