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
@@ -0,0 +1,107 @@
import type { Session } from '@opencode-ai/sdk/v2';
export const ACTIVE_NOW_STORAGE_KEY = 'oc.sessions.activeNow';
export const ACTIVE_NOW_MAX_AGE_MS = 36 * 60 * 60 * 1000;
export type ActiveNowEntry = {
sessionId: string;
};
const isSubtaskSession = (session: Session): boolean => {
return Boolean((session as Session & { parentID?: string | null }).parentID);
};
const isArchivedSession = (session: Session): boolean => {
return Boolean(session.time?.archived);
};
const getSessionUpdatedAt = (session: Session): number => {
const updated = session.time?.updated;
const created = session.time?.created;
if (typeof updated === 'number' && Number.isFinite(updated)) {
return updated;
}
if (typeof created === 'number' && Number.isFinite(created)) {
return created;
}
return 0;
};
export const readActiveNowEntries = (storage: Storage): ActiveNowEntry[] => {
try {
const raw = storage.getItem(ACTIVE_NOW_STORAGE_KEY);
if (!raw) {
return [];
}
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) {
return [];
}
const seen = new Set<string>();
const next: ActiveNowEntry[] = [];
parsed.forEach((item) => {
const sessionId = typeof item === 'string'
? item
: (item && typeof item === 'object' && 'sessionId' in item && typeof item.sessionId === 'string' ? item.sessionId : null);
if (!sessionId || seen.has(sessionId)) {
return;
}
seen.add(sessionId);
next.push({ sessionId });
});
return next;
} catch {
return [];
}
};
export const persistActiveNowEntries = (storage: Storage, entries: ActiveNowEntry[]): void => {
try {
storage.setItem(ACTIVE_NOW_STORAGE_KEY, JSON.stringify(entries));
} catch {
// ignored
}
};
export const pruneActiveNowEntries = (
entries: ActiveNowEntry[],
sessionsById: Map<string, Session>,
now = Date.now(),
): ActiveNowEntry[] => {
const minUpdatedAt = now - ACTIVE_NOW_MAX_AGE_MS;
return entries.filter((entry) => {
const session = sessionsById.get(entry.sessionId);
if (!session) {
return true;
}
if (isArchivedSession(session)) {
return false;
}
return getSessionUpdatedAt(session) >= minUpdatedAt;
});
};
export const addActiveNowSession = (entries: ActiveNowEntry[], sessionId: string): ActiveNowEntry[] => {
if (!sessionId || entries.some((entry) => entry.sessionId === sessionId)) {
return entries;
}
return [{ sessionId }, ...entries];
};
export const sortSessionsByUpdated = (sessions: Session[]): Session[] => {
return [...sessions].sort((a, b) => getSessionUpdatedAt(b) - getSessionUpdatedAt(a));
};
export const deriveActiveNowSessions = (
entries: ActiveNowEntry[],
sessionsById: Map<string, Session>,
): Session[] => {
const sessions = entries
.map((entry) => sessionsById.get(entry.sessionId) ?? null)
.filter((session): session is Session => Boolean(session))
.filter((session) => !isArchivedSession(session))
.filter((session) => !isSubtaskSession(session));
return sortSessionsByUpdated(sessions);
};
export const getSessionUpdatedAtMs = getSessionUpdatedAt;