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:
committed by
GitHub
co-authored by
Iuliia Ivashko
parent
359879153a
commit
321cc7252a
@@ -60,6 +60,31 @@ const getConfigDirectory = (): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const AGENTS_LOAD_CACHE_TTL_MS = 5000;
|
||||
const DEFAULT_AGENTS_CACHE_KEY = '__default__';
|
||||
const agentsLastLoadedAt = new Map<string, number>();
|
||||
const agentsLoadInFlight = new Map<string, Promise<boolean>>();
|
||||
|
||||
const getAgentsCacheKey = (directory: string | null): string => {
|
||||
return directory?.trim() || DEFAULT_AGENTS_CACHE_KEY;
|
||||
};
|
||||
|
||||
const buildAgentsSignature = (agents: Agent[]): string => {
|
||||
return agents
|
||||
.map((agent) => {
|
||||
const extended = agent as AgentWithExtras;
|
||||
return [
|
||||
agent.name,
|
||||
extended.scope ?? '',
|
||||
extended.group ?? '',
|
||||
extended.description ?? '',
|
||||
String(extended.hidden === true),
|
||||
String(extended.native === true),
|
||||
].join('|');
|
||||
})
|
||||
.join('||');
|
||||
};
|
||||
|
||||
export type AgentScope = 'user' | 'project';
|
||||
|
||||
export interface AgentConfig {
|
||||
@@ -183,74 +208,100 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
},
|
||||
|
||||
loadAgents: async () => {
|
||||
set({ isLoading: true });
|
||||
const previousAgents = get().agents;
|
||||
const configDirectory = getConfigDirectory();
|
||||
const cacheKey = getAgentsCacheKey(configDirectory);
|
||||
const now = Date.now();
|
||||
const loadedAt = agentsLastLoadedAt.get(cacheKey) ?? 0;
|
||||
const hasCachedAgents = get().agents.length > 0;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const configDirectory = getConfigDirectory();
|
||||
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
|
||||
|
||||
// Ensure we list agents using the correct project context
|
||||
const agents = await opencodeClient.withDirectory(configDirectory, () => opencodeClient.listAgents());
|
||||
|
||||
const agentsWithScope = await Promise.all(
|
||||
agents.map(async (agent) => {
|
||||
try {
|
||||
// Force no-cache to ensure we get the latest scope info
|
||||
const response = await fetch(`/api/config/agents/${encodeURIComponent(agent.name)}${queryParams}`, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
...(configDirectory ? { 'x-opencode-directory': configDirectory } : {}),
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Prioritize explicit scope from server response
|
||||
let scope = data.scope;
|
||||
|
||||
// Fallback to deducing from sources if top-level scope is missing
|
||||
if (!scope && data.sources) {
|
||||
const sources = data.sources;
|
||||
scope = (sources.md?.exists ? sources.md.scope : undefined)
|
||||
?? (sources.json?.exists ? sources.json.scope : undefined)
|
||||
?? sources.md?.scope
|
||||
?? sources.json?.scope;
|
||||
}
|
||||
|
||||
// Parse subfolder group from file path
|
||||
const mdPath: string | null | undefined = data.sources?.md?.path;
|
||||
const group = parseAgentGroup(mdPath);
|
||||
|
||||
if (scope === 'project' || scope === 'user') {
|
||||
return { ...agent, scope: scope as AgentScope, group };
|
||||
}
|
||||
|
||||
// Explicitly set null scope if not found, to clear stale state
|
||||
return { ...agent, scope: undefined, group };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[AgentsStore] Failed to fetch config for agent ${agent.name}:`, err);
|
||||
}
|
||||
return agent;
|
||||
})
|
||||
);
|
||||
|
||||
if (JSON.stringify(previousAgents) !== JSON.stringify(agentsWithScope)) {
|
||||
set({ agents: agentsWithScope, isLoading: false });
|
||||
} else {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
if (hasCachedAgents && now - loadedAt < AGENTS_LOAD_CACHE_TTL_MS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const inFlight = agentsLoadInFlight.get(cacheKey);
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const request = (async () => {
|
||||
set({ isLoading: true });
|
||||
const previousAgents = get().agents;
|
||||
const previousSignature = buildAgentsSignature(previousAgents);
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
|
||||
|
||||
// Ensure we list agents using the correct project context
|
||||
const agents = await opencodeClient.withDirectory(configDirectory, () => opencodeClient.listAgents());
|
||||
|
||||
const agentsWithScope = await Promise.all(
|
||||
agents.map(async (agent) => {
|
||||
try {
|
||||
// Force no-cache to ensure we get the latest scope info
|
||||
const response = await fetch(`/api/config/agents/${encodeURIComponent(agent.name)}${queryParams}`, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
...(configDirectory ? { 'x-opencode-directory': configDirectory } : {}),
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Prioritize explicit scope from server response
|
||||
let scope = data.scope;
|
||||
|
||||
// Fallback to deducing from sources if top-level scope is missing
|
||||
if (!scope && data.sources) {
|
||||
const sources = data.sources;
|
||||
scope = (sources.md?.exists ? sources.md.scope : undefined)
|
||||
?? (sources.json?.exists ? sources.json.scope : undefined)
|
||||
?? sources.md?.scope
|
||||
?? sources.json?.scope;
|
||||
}
|
||||
|
||||
// Parse subfolder group from file path
|
||||
const mdPath: string | null | undefined = data.sources?.md?.path;
|
||||
const group = parseAgentGroup(mdPath);
|
||||
|
||||
if (scope === 'project' || scope === 'user') {
|
||||
return { ...agent, scope: scope as AgentScope, group };
|
||||
}
|
||||
|
||||
// Explicitly set null scope if not found, to clear stale state
|
||||
return { ...agent, scope: undefined, group };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[AgentsStore] Failed to fetch config for agent ${agent.name}:`, err);
|
||||
}
|
||||
return agent;
|
||||
})
|
||||
);
|
||||
|
||||
const nextSignature = buildAgentsSignature(agentsWithScope);
|
||||
if (previousSignature !== nextSignature) {
|
||||
set({ agents: agentsWithScope, isLoading: false });
|
||||
} else {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
agentsLastLoadedAt.set(cacheKey, Date.now());
|
||||
return true;
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
set({ isLoading: false });
|
||||
return false;
|
||||
})();
|
||||
|
||||
agentsLoadInFlight.set(cacheKey, request);
|
||||
try {
|
||||
return await request;
|
||||
} finally {
|
||||
agentsLoadInFlight.delete(cacheKey);
|
||||
}
|
||||
|
||||
set({ isLoading: false });
|
||||
return false;
|
||||
},
|
||||
|
||||
createAgent: async (config: AgentConfig) => {
|
||||
|
||||
Reference in New Issue
Block a user