Major UI refresh: sidebar redesign, theme expansion, and chat performance optimizations (#706)

## Summary
Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements.

## Key Changes

**Sidebar & Navigation Redesign**
- Redesigned sessions sidebar layout with unified button primitives
- Added activity sections with project grouping and improved session organization
- Refined sidebar corners, spacing, and visual hierarchy
- Removed NavRail component in favor of streamlined sidebar
- Stabilized sessions bar toggle position in fullscreen mode

**Performance Optimizations**
- Reduced chat streaming CPU usage and storage churn
- Optimized task tool polling and live timers with debouncing
- Prevented chat state races and reduced background request load
- Debounced draft writes and coalesced session reloads
- Optimized message store updates and turn tracking

**Theme & Visual System**
- Added theme-aware window corners (desktop) and border radius tokens
- Introduced glassmorphism effects on desktop sidebar
- Added backdrop blur to UI elements

**Chat Experience**
- Added session-based permission auto-accept toggle in chat input
- Polished permission shield UX with improved icon sizing and spacing
- Fixed chat scroll-to-bottom behavior and timeline tracking
- Enhanced tool output display with better path label detection
- Removed duplicate draft context details in chat header
- Added text selection menu to chat messages

**Git Improvements**
- Refreshed git history visual design with cleaner dividers
- Added remote removal action in sync selector
- Stabilized git polling to prevent excessive requests
- Improved tool output rendering for git operations

**Settings & Panels**
- Fixed mobile scrolling on settings pages
- Made outside-click settings close instantly
- Reduced settings load churn and CPU spikes
- Improved services dropdown layout and spacing
- Softened panel resize handles

**Desktop Integration**
- Synced macOS window theme with app theme
- Restored window dragging in sidebar header zones
- Fixed system window corners on macOS
- Improved header session metadata and action controls

**Button & Component Standardization**
- Unified button primitives across all components
- Standardized destructive action patterns
- Removed unused button variants (button-large, button-small)
- Aligned context tab close hit areas

---------

Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
This commit is contained in:
Bohdan Triapitsyn
2026-03-20 01:01:03 +02:00
committed by GitHub
co-authored by Iuliia Ivashko
parent 359879153a
commit 321cc7252a
222 changed files with 8575 additions and 5456 deletions
+92 -17
View File
@@ -318,6 +318,33 @@ const withShellBridgeDetails = (message: ChatMessageEntry, details: ShellBridgeD
};
};
const normalizedMessageBySource = new WeakMap<ChatMessageEntry, ChatMessageEntry>();
const getNormalizedMessageForDisplay = (message: ChatMessageEntry): ChatMessageEntry => {
const cached = normalizedMessageBySource.get(message);
if (cached) {
return cached;
}
const filteredParts = filterSyntheticParts(message.parts);
const normalized = filteredParts === message.parts
? message
: {
...message,
parts: filteredParts,
};
normalizedMessageBySource.set(message, normalized);
return normalized;
};
const isAssistantTextOnlyMessage = (message: ChatMessageEntry): boolean => {
if (resolveMessageRole(message) !== 'assistant') {
return false;
}
return message.parts.length > 0 && message.parts.every((part) => part?.type === 'text');
};
interface MessageListProps {
sessionKey: string;
turnStart: number;
@@ -813,6 +840,11 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
previousOrder: string[];
animatedIds: Set<string>;
}>({ sessionKey: undefined, previousOrder: [], animatedIds: new Set() });
const baseDisplayCacheRef = React.useRef<{
input: ChatMessageEntry[];
output: ChatMessageEntry[];
outputIndexById: Map<string, number>;
} | null>(null);
const stableOnMessageContentChange = useStableEvent(onMessageContentChange);
const stableGetAnimationHandlers = useStableEvent(getAnimationHandlers);
@@ -843,8 +875,51 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
const baseDisplayMessages = React.useMemo(() => {
const seenIdsFromTail = new Set<string>();
const cached = baseDisplayCacheRef.current;
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
const canUseTailFastPath = Boolean(lastMessage && isAssistantTextOnlyMessage(lastMessage));
if (cached && canUseTailFastPath && cached.input.length === messages.length && messages.length > 0) {
let changedCount = 0;
let changedIndex = -1;
let idsStable = true;
for (let index = 0; index < messages.length; index += 1) {
if (messages[index]?.info?.id !== cached.input[index]?.info?.id) {
idsStable = false;
break;
}
if (messages[index] !== cached.input[index]) {
changedCount += 1;
changedIndex = index;
if (changedCount > 1) {
break;
}
}
}
if (idsStable && changedCount === 1 && changedIndex === messages.length - 1) {
const changedMessage = messages[changedIndex];
const previousMessage = changedIndex > 0 ? messages[changedIndex - 1] : undefined;
const bridgeSensitive = isUserSubtaskMessage(previousMessage) || isUserShellMarkerMessage(previousMessage);
if (changedMessage && isAssistantTextOnlyMessage(changedMessage) && !bridgeSensitive) {
const outputIndex = cached.outputIndexById.get(changedMessage.info.id);
if (outputIndex !== undefined) {
const nextOutput = [...cached.output];
nextOutput[outputIndex] = getNormalizedMessageForDisplay(changedMessage);
baseDisplayCacheRef.current = {
input: messages,
output: nextOutput,
outputIndexById: cached.outputIndexById,
};
return nextOutput;
}
}
}
}
const seenIdsFromTail = new Set<string>();
const dedupedMessages: ChatMessageEntry[] = [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
@@ -855,26 +930,13 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
}
seenIdsFromTail.add(messageId);
}
dedupedMessages.push(message);
dedupedMessages.push(getNormalizedMessageForDisplay(message));
}
dedupedMessages.reverse();
const normalizedMessages = dedupedMessages
.map((message) => {
const filteredParts = filterSyntheticParts(message.parts);
const normalized = filteredParts === message.parts
? message
: {
...message,
parts: filteredParts,
};
return normalized;
});
const output: ChatMessageEntry[] = [];
for (let index = 0; index < normalizedMessages.length; index += 1) {
const current = normalizedMessages[index];
for (let index = 0; index < dedupedMessages.length; index += 1) {
const current = dedupedMessages[index];
const previous = output.length > 0 ? output[output.length - 1] : undefined;
if (isUserSubtaskMessage(previous)) {
@@ -896,6 +958,19 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
output.push(current);
}
const outputIndexById = new Map<string, number>();
output.forEach((message, index) => {
const id = message.info?.id;
if (typeof id === 'string' && id.length > 0) {
outputIndexById.set(id, index);
}
});
baseDisplayCacheRef.current = {
input: messages,
output,
outputIndexById,
};
return output;
}, [messages]);