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:
shekohex
2026-02-27 20:03:42 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent d6b8f28e6f
commit 1d8ff97c95
1134 changed files with 14091 additions and 2005 deletions
@@ -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>