Files
openchamber/packages/ui/src/stores/useMcpConfigStore.ts
T
Bohdan TriapitsynandIuliia Ivashko 321cc7252a 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>
2026-03-20 01:01:03 +02:00

321 lines
11 KiB
TypeScript

import { create } from 'zustand';
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
import { getSafeStorage } from './utils/safeStorage';
import {
startConfigUpdate,
finishConfigUpdate,
} from '@/lib/configUpdate';
import { refreshAfterOpenCodeRestart } from '@/stores/useAgentsStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { opencodeClient } from '@/lib/opencode/client';
export type McpScope = 'user' | 'project';
const getConfigDirectory = (): string | null => {
try {
const projectsStore = useProjectsStore.getState();
const activeProject = projectsStore.getActiveProject?.();
if (activeProject?.path?.trim()) {
return activeProject.path.trim();
}
const clientDir = opencodeClient.getDirectory();
if (clientDir?.trim()) {
return clientDir.trim();
}
} catch (err) {
console.warn('[McpConfigStore] Error resolving config directory:', err);
}
return null;
};
// ============== TYPES ==============
export interface McpLocalConfig {
type: 'local';
command: string[];
environment?: Record<string, string>;
enabled: boolean;
}
export interface McpRemoteConfig {
type: 'remote';
url: string;
environment?: Record<string, string>;
enabled: boolean;
}
export type McpServerConfig = (McpLocalConfig | McpRemoteConfig) & { name: string };
export type McpServerWithScope = McpServerConfig & { scope?: McpScope | null };
export interface McpDraft {
name: string;
scope: McpScope;
type: 'local' | 'remote';
command: string[];
url: string;
environment: Array<{ key: string; value: string }>;
enabled: boolean;
}
// ============== HELPERS ==============
export const envRecordToArray = (env?: Record<string, string>): Array<{ key: string; value: string }> => {
if (!env) return [];
return Object.entries(env).map(([key, value]) => ({ key, value }));
};
export const envArrayToRecord = (arr: Array<{ key: string; value: string }>): Record<string, string> | undefined => {
const filtered = arr.filter((e) => e.key.trim());
if (filtered.length === 0) return undefined;
return Object.fromEntries(filtered.map((e) => [e.key.trim(), e.value]));
};
const CLIENT_RELOAD_DELAY_MS = 800;
const MCP_LOAD_CACHE_TTL_MS = 5000;
const DEFAULT_MCP_CACHE_KEY = '__default__';
const mcpLastLoadedAt = new Map<string, number>();
const mcpLoadInFlight = new Map<string, Promise<boolean>>();
const getMcpCacheKey = (directory: string | null): string => {
return directory?.trim() || DEFAULT_MCP_CACHE_KEY;
};
// ============== STORE ==============
interface McpConfigStore {
mcpServers: McpServerWithScope[];
selectedMcpName: string | null;
isLoading: boolean;
mcpDraft: McpDraft | null;
setSelectedMcp: (name: string | null) => void;
setMcpDraft: (draft: McpDraft | null) => void;
loadMcpConfigs: () => Promise<boolean>;
createMcp: (config: McpDraft) => Promise<boolean>;
updateMcp: (name: string, config: Partial<McpDraft>) => Promise<boolean>;
deleteMcp: (name: string) => Promise<boolean>;
getMcpByName: (name: string) => McpServerWithScope | undefined;
}
export const useMcpConfigStore = create<McpConfigStore>()(
devtools(
persist(
(set, get) => ({
mcpServers: [],
selectedMcpName: null,
isLoading: false,
mcpDraft: null,
setSelectedMcp: (name) => set({ selectedMcpName: name }),
setMcpDraft: (draft) => set({ mcpDraft: draft }),
loadMcpConfigs: async () => {
const configDirectory = getConfigDirectory();
const cacheKey = getMcpCacheKey(configDirectory);
const now = Date.now();
const loadedAt = mcpLastLoadedAt.get(cacheKey) ?? 0;
const hasCachedConfigs = get().mcpServers.length > 0;
if (hasCachedConfigs && now - loadedAt < MCP_LOAD_CACHE_TTL_MS) {
return true;
}
const inFlight = mcpLoadInFlight.get(cacheKey);
if (inFlight) {
return inFlight;
}
const request = (async () => {
set({ isLoading: true });
try {
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
const response = await fetch(`/api/config/mcp${queryParams}`, {
headers: configDirectory ? { 'x-opencode-directory': configDirectory } : undefined,
});
if (!response.ok) {
throw new Error('Failed to load MCP configs');
}
const data: McpServerWithScope[] = await response.json();
set({ mcpServers: data, isLoading: false });
mcpLastLoadedAt.set(cacheKey, Date.now());
return true;
} catch (error) {
console.error('[McpConfigStore] Failed to load MCP configs:', error);
set({ isLoading: false });
return false;
}
})();
mcpLoadInFlight.set(cacheKey, request);
try {
return await request;
} finally {
mcpLoadInFlight.delete(cacheKey);
}
},
createMcp: async (config: McpDraft) => {
startConfigUpdate('Creating MCP server configuration…');
let requiresReload = false;
try {
const body = buildMcpBody(config);
const configDirectory = getConfigDirectory();
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
const response = await fetch(`/api/config/mcp/${encodeURIComponent(config.name)}${queryParams}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(configDirectory ? { 'x-opencode-directory': configDirectory } : {}),
},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(payload?.error || 'Failed to create MCP server');
}
if (payload?.requiresReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
message: payload.message,
delayMs: payload.reloadDelayMs ?? CLIENT_RELOAD_DELAY_MS,
scopes: ['all'],
});
return true;
}
await get().loadMcpConfigs();
return true;
} catch (error) {
console.error('[McpConfigStore] Failed to create MCP:', error);
return false;
} finally {
if (!requiresReload) finishConfigUpdate();
}
},
updateMcp: async (name: string, config: Partial<McpDraft>) => {
startConfigUpdate('Updating MCP server configuration…');
let requiresReload = false;
try {
const body = buildMcpBody(config);
const configDirectory = getConfigDirectory();
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
const response = await fetch(`/api/config/mcp/${encodeURIComponent(name)}${queryParams}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...(configDirectory ? { 'x-opencode-directory': configDirectory } : {}),
},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(payload?.error || 'Failed to update MCP server');
}
if (payload?.requiresReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
message: payload.message,
delayMs: payload.reloadDelayMs ?? CLIENT_RELOAD_DELAY_MS,
scopes: ['all'],
});
return true;
}
await get().loadMcpConfigs();
return true;
} catch (error) {
console.error('[McpConfigStore] Failed to update MCP:', error);
throw error;
} finally {
if (!requiresReload) finishConfigUpdate();
}
},
deleteMcp: async (name: string) => {
startConfigUpdate('Deleting MCP server configuration…');
let requiresReload = false;
try {
const configDirectory = getConfigDirectory();
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
const response = await fetch(`/api/config/mcp/${encodeURIComponent(name)}${queryParams}`, {
method: 'DELETE',
headers: configDirectory ? { 'x-opencode-directory': configDirectory } : undefined,
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(payload?.error || 'Failed to delete MCP server');
}
if (payload?.requiresReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
message: payload.message,
delayMs: payload.reloadDelayMs ?? CLIENT_RELOAD_DELAY_MS,
scopes: ['all'],
});
return true;
}
if (get().selectedMcpName === name) {
set({ selectedMcpName: null });
}
await get().loadMcpConfigs();
return true;
} catch (error) {
console.error('[McpConfigStore] Failed to delete MCP:', error);
return false;
} finally {
if (!requiresReload) finishConfigUpdate();
}
},
getMcpByName: (name: string) => {
return get().mcpServers.find((s) => s.name === name);
},
}),
{
name: 'mcp-config-store',
storage: createJSONStorage(() => getSafeStorage()),
partialize: (state) => ({ selectedMcpName: state.selectedMcpName }),
},
),
{ name: 'mcp-config-store' },
),
);
// ============== HELPERS ==============
function buildMcpBody(config: Partial<McpDraft>): Record<string, unknown> {
const body: Record<string, unknown> = {};
if (config.scope !== undefined) body.scope = config.scope;
if (config.type !== undefined) body.type = config.type;
if (config.type === 'local' || config.command !== undefined) {
body.command = (config.command ?? []).filter((s) => s.trim());
}
if (config.type === 'remote' || config.url !== undefined) {
body.url = config.url?.trim() ?? '';
}
if (config.environment !== undefined) {
body.environment = envArrayToRecord(config.environment) ?? {};
}
if (config.enabled !== undefined) {
body.enabled = config.enabled;
}
return body;
}