## 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>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import React from 'react';
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
const SESSION_PREFETCH_HOVER_DELAY_MS = 180;
|
|
const SESSION_PREFETCH_CONCURRENCY = 1;
|
|
const SESSION_PREFETCH_PENDING_LIMIT = 6;
|
|
|
|
type Args = {
|
|
currentSessionId: string | null;
|
|
sortedSessions: Session[];
|
|
recentSessionIds?: string[];
|
|
loadMessages: (sessionId: string, limit?: number) => Promise<void>;
|
|
};
|
|
|
|
export const useSessionPrefetch = ({ currentSessionId, sortedSessions, recentSessionIds = [], loadMessages }: Args): void => {
|
|
const sessionPrefetchTimersRef = React.useRef<Map<string, number>>(new Map());
|
|
const sessionPrefetchQueueRef = React.useRef<string[]>([]);
|
|
const sessionPrefetchInFlightRef = React.useRef<Set<string>>(new Set());
|
|
|
|
const pumpSessionPrefetchQueue = React.useCallback(() => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
while (sessionPrefetchInFlightRef.current.size < SESSION_PREFETCH_CONCURRENCY && sessionPrefetchQueueRef.current.length > 0) {
|
|
const nextSessionId = sessionPrefetchQueueRef.current.shift();
|
|
if (!nextSessionId) {
|
|
break;
|
|
}
|
|
|
|
const state = useSessionStore.getState();
|
|
if (state.currentSessionId === nextSessionId) {
|
|
continue;
|
|
}
|
|
|
|
const hasMessages = state.messages.has(nextSessionId);
|
|
const historyMeta = state.sessionHistoryMeta.get(nextSessionId);
|
|
const isHydrated = hasMessages && typeof historyMeta?.complete === 'boolean';
|
|
if (isHydrated) {
|
|
continue;
|
|
}
|
|
|
|
sessionPrefetchInFlightRef.current.add(nextSessionId);
|
|
void loadMessages(nextSessionId)
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
sessionPrefetchInFlightRef.current.delete(nextSessionId);
|
|
pumpSessionPrefetchQueue();
|
|
});
|
|
}
|
|
}, [loadMessages]);
|
|
|
|
const scheduleSessionPrefetch = React.useCallback((sessionId: string | null | undefined) => {
|
|
if (!sessionId || sessionId === currentSessionId || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const state = useSessionStore.getState();
|
|
const hasMessages = state.messages.has(sessionId);
|
|
const historyMeta = state.sessionHistoryMeta.get(sessionId);
|
|
const isHydrated = hasMessages && typeof historyMeta?.complete === 'boolean';
|
|
if (isHydrated) {
|
|
return;
|
|
}
|
|
|
|
if (sessionPrefetchInFlightRef.current.has(sessionId)) {
|
|
return;
|
|
}
|
|
|
|
if (sessionPrefetchQueueRef.current.includes(sessionId)) {
|
|
return;
|
|
}
|
|
|
|
if (sessionPrefetchQueueRef.current.length >= SESSION_PREFETCH_PENDING_LIMIT) {
|
|
sessionPrefetchQueueRef.current.shift();
|
|
}
|
|
|
|
const existingTimer = sessionPrefetchTimersRef.current.get(sessionId);
|
|
if (existingTimer !== undefined) {
|
|
window.clearTimeout(existingTimer);
|
|
}
|
|
|
|
const timer = window.setTimeout(() => {
|
|
sessionPrefetchTimersRef.current.delete(sessionId);
|
|
sessionPrefetchQueueRef.current.push(sessionId);
|
|
pumpSessionPrefetchQueue();
|
|
}, SESSION_PREFETCH_HOVER_DELAY_MS);
|
|
sessionPrefetchTimersRef.current.set(sessionId, timer);
|
|
}, [currentSessionId, pumpSessionPrefetchQueue]);
|
|
|
|
React.useEffect(() => {
|
|
if (!currentSessionId || sortedSessions.length === 0) {
|
|
return;
|
|
}
|
|
const currentIndex = sortedSessions.findIndex((session) => session.id === currentSessionId);
|
|
if (currentIndex < 0) {
|
|
return;
|
|
}
|
|
scheduleSessionPrefetch(sortedSessions[currentIndex - 1]?.id);
|
|
scheduleSessionPrefetch(sortedSessions[currentIndex + 1]?.id);
|
|
}, [currentSessionId, scheduleSessionPrefetch, sortedSessions]);
|
|
|
|
React.useEffect(() => {
|
|
if (!currentSessionId || recentSessionIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const currentIndex = recentSessionIds.indexOf(currentSessionId);
|
|
if (currentIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
scheduleSessionPrefetch(recentSessionIds[currentIndex - 1]);
|
|
scheduleSessionPrefetch(recentSessionIds[currentIndex + 1]);
|
|
}, [currentSessionId, recentSessionIds, scheduleSessionPrefetch]);
|
|
|
|
React.useEffect(() => {
|
|
const prefetchTimers = sessionPrefetchTimersRef.current;
|
|
return () => {
|
|
prefetchTimers.forEach((timer) => {
|
|
clearTimeout(timer);
|
|
});
|
|
prefetchTimers.clear();
|
|
sessionPrefetchQueueRef.current = [];
|
|
};
|
|
}, []);
|
|
};
|