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
@@ -1,20 +1,27 @@
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist, createJSONStorage } from "zustand/middleware";
|
||||
import { opencodeClient } from "@/lib/opencode/client";
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client";
|
||||
import type { PermissionRequest, PermissionResponse } from "@/types/permission";
|
||||
import { isEditPermissionType, getAgentDefaultEditPermission } from "./utils/permissionUtils";
|
||||
import {
|
||||
normalizeDirectory,
|
||||
type PermissionAutoAcceptMap,
|
||||
} from "./utils/permissionAutoAccept";
|
||||
import { getSafeStorage } from "./utils/safeStorage";
|
||||
import { useMessageStore } from "./messageStore";
|
||||
import { useSessionStore } from "./sessionStore";
|
||||
|
||||
interface PermissionState {
|
||||
permissions: Map<string, PermissionRequest[]>;
|
||||
autoAccept: PermissionAutoAcceptMap;
|
||||
}
|
||||
|
||||
interface PermissionActions {
|
||||
addPermission: (permission: PermissionRequest, contextData?: { currentAgentContext?: Map<string, string>, sessionAgentSelections?: Map<string, string>, getSessionAgentEditMode?: (sessionId: string, agentName: string | undefined) => string }) => void;
|
||||
addPermission: (permission: PermissionRequest) => void;
|
||||
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
|
||||
dismissPermission: (sessionId: string, requestId: string) => void;
|
||||
isSessionAutoAccepting: (sessionId: string) => boolean;
|
||||
setSessionAutoAccept: (sessionId: string, enabled: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
type PermissionStore = PermissionState & PermissionActions;
|
||||
@@ -53,14 +60,111 @@ const executeWithPermissionDirectory = async <T>(sessionId: string, operation: (
|
||||
return operation();
|
||||
};
|
||||
|
||||
const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
|
||||
const map = new Map<string, Session>();
|
||||
for (const session of sessions) {
|
||||
map.set(session.id, session);
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
let current: string | undefined = sessionID;
|
||||
while (current && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
result.push(current);
|
||||
current = map.get(current)?.parentID;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const autoRespondsPermissionBySession = (
|
||||
autoAccept: PermissionAutoAcceptMap,
|
||||
sessions: Session[],
|
||||
sessionID: string,
|
||||
): boolean => {
|
||||
for (const id of resolveLineage(sessionID, sessions)) {
|
||||
if (id in autoAccept) {
|
||||
return autoAccept[id] === true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const shouldAutoRespond = (permission: PermissionRequest, autoAccept: PermissionAutoAcceptMap): boolean => {
|
||||
if (!permission?.sessionID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessionStore = useSessionStore.getState();
|
||||
const sessions = sessionStore.sessions;
|
||||
return autoRespondsPermissionBySession(
|
||||
autoAccept,
|
||||
sessions,
|
||||
permission.sessionID,
|
||||
);
|
||||
};
|
||||
|
||||
const collectPermissionDirectories = (fallbackDirectory?: string | null): string[] => {
|
||||
const sessionStore = useSessionStore.getState();
|
||||
const dirs = new Set<string>();
|
||||
const fallback = normalizeDirectory(fallbackDirectory);
|
||||
if (fallback) {
|
||||
dirs.add(fallback);
|
||||
}
|
||||
|
||||
const currentDirectory = normalizeDirectory(opencodeClient.getDirectory());
|
||||
if (currentDirectory) {
|
||||
dirs.add(currentDirectory);
|
||||
}
|
||||
|
||||
for (const session of sessionStore.sessions) {
|
||||
const normalized = normalizeDirectory((session as { directory?: string | null }).directory);
|
||||
if (normalized) {
|
||||
dirs.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(dirs);
|
||||
};
|
||||
|
||||
const reconcilePendingAutoAccept = async (
|
||||
autoAccept: PermissionAutoAcceptMap,
|
||||
fallbackDirectory?: string | null,
|
||||
) => {
|
||||
const directories = collectPermissionDirectories(fallbackDirectory);
|
||||
if (directories.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = await opencodeClient.listPendingPermissions({ directories });
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const request of pending) {
|
||||
if (!request?.sessionID || !request?.id) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldAutoRespond(request, autoAccept)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await executeWithPermissionDirectory(request.sessionID, () => opencodeClient.replyToPermission(request.id, 'once'));
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const usePermissionStore = create<PermissionStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
|
||||
permissions: new Map(),
|
||||
autoAccept: {},
|
||||
|
||||
addPermission: (permission: PermissionRequest, contextData?: { currentAgentContext?: Map<string, string>, sessionAgentSelections?: Map<string, string>, getSessionAgentEditMode?: (sessionId: string, agentName: string | undefined) => string }) => {
|
||||
addPermission: (permission: PermissionRequest) => {
|
||||
const sessionId = permission.sessionID;
|
||||
if (!sessionId) {
|
||||
return;
|
||||
@@ -71,28 +175,7 @@ export const usePermissionStore = create<PermissionStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionType = permission.permission?.toLowerCase?.() ?? null;
|
||||
|
||||
let agentName = contextData?.currentAgentContext?.get(sessionId);
|
||||
if (!agentName) {
|
||||
agentName = contextData?.sessionAgentSelections?.get(sessionId) ?? undefined;
|
||||
}
|
||||
if (!agentName) {
|
||||
if (typeof window !== "undefined") {
|
||||
const configStore = window.__zustand_config_store__;
|
||||
if (configStore?.getState) {
|
||||
agentName = configStore.getState().currentAgentName ?? undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultMode = getAgentDefaultEditPermission(agentName);
|
||||
const effectiveMode = contextData?.getSessionAgentEditMode?.(sessionId, agentName) ?? defaultMode;
|
||||
|
||||
const shouldAutoApprove = (effectiveMode === 'allow' || effectiveMode === 'full')
|
||||
&& isEditPermissionType(permissionType);
|
||||
|
||||
if (shouldAutoApprove) {
|
||||
if (shouldAutoRespond(permission, get().autoAccept)) {
|
||||
get().respondToPermission(sessionId, permission.id, 'once').catch(() => {
|
||||
|
||||
});
|
||||
@@ -134,21 +217,58 @@ export const usePermissionStore = create<PermissionStore>()(
|
||||
return { permissions: newPermissions };
|
||||
});
|
||||
},
|
||||
|
||||
isSessionAutoAccepting: (sessionId: string) => {
|
||||
if (!sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessions = useSessionStore.getState().sessions;
|
||||
return autoRespondsPermissionBySession(get().autoAccept, sessions, sessionId);
|
||||
},
|
||||
|
||||
setSessionAutoAccept: async (sessionId: string, enabled: boolean) => {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
autoAccept: {
|
||||
...state.autoAccept,
|
||||
[sessionId]: enabled,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const sessionDirectory = useSessionStore.getState().getDirectoryForSession(sessionId);
|
||||
void reconcilePendingAutoAccept(get().autoAccept, sessionDirectory);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "permission-store",
|
||||
storage: createJSONStorage(() => getSafeStorage()),
|
||||
partialize: (state) => ({
|
||||
permissions: Array.from(state.permissions.entries()),
|
||||
autoAccept: state.autoAccept,
|
||||
}),
|
||||
merge: (persistedState, currentState) => {
|
||||
if (!isRecord(persistedState)) {
|
||||
return currentState;
|
||||
}
|
||||
const entries = sanitizePermissionEntries(persistedState.permissions);
|
||||
const autoAccept = isRecord(persistedState.autoAccept)
|
||||
? Object.fromEntries(
|
||||
Object.entries(persistedState.autoAccept).filter((entry): entry is [string, boolean] => {
|
||||
return typeof entry[0] === "string" && typeof entry[1] === "boolean";
|
||||
}),
|
||||
)
|
||||
: {};
|
||||
return {
|
||||
...currentState,
|
||||
permissions: new Map(entries),
|
||||
autoAccept,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user