## 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>
177 lines
4.5 KiB
TypeScript
177 lines
4.5 KiB
TypeScript
const DB_NAME = 'openchamber-message-cursors';
|
|
const STORE_NAME = 'cursors';
|
|
const DB_VERSION = 1;
|
|
const FALLBACK_KEY = 'openchamber.messageCursors';
|
|
|
|
type CursorRecord = {
|
|
messageId: string;
|
|
completedAt: number;
|
|
};
|
|
|
|
const isBrowser = () => typeof window !== 'undefined';
|
|
|
|
const hasIndexedDbSupport = () => {
|
|
return isBrowser() && typeof indexedDB !== 'undefined';
|
|
};
|
|
|
|
const openDatabase = (): Promise<IDBDatabase> => {
|
|
if (!hasIndexedDbSupport()) {
|
|
return Promise.reject(new Error('IndexedDB not supported'));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME);
|
|
}
|
|
};
|
|
|
|
request.onerror = () => {
|
|
reject(request.error ?? new Error('Failed to open IndexedDB'));
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
resolve(request.result);
|
|
};
|
|
});
|
|
};
|
|
|
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
|
|
const getDatabase = (): Promise<IDBDatabase> => {
|
|
if (!dbPromise) {
|
|
dbPromise = openDatabase()
|
|
.then((db) => {
|
|
db.onclose = () => {
|
|
dbPromise = null;
|
|
};
|
|
db.onversionchange = () => {
|
|
db.close();
|
|
};
|
|
return db;
|
|
})
|
|
.catch((error: unknown) => {
|
|
dbPromise = null;
|
|
throw error;
|
|
});
|
|
}
|
|
return dbPromise;
|
|
};
|
|
|
|
const readFallback = (): Record<string, CursorRecord> => {
|
|
if (!isBrowser()) {
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(FALLBACK_KEY);
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
const parsed = JSON.parse(raw) as Record<string, CursorRecord>;
|
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
const writeFallback = (map: Record<string, CursorRecord>) => {
|
|
if (!isBrowser()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
window.localStorage.setItem(FALLBACK_KEY, JSON.stringify(map));
|
|
} catch { /* ignored */ }
|
|
};
|
|
|
|
export const saveSessionCursor = async (
|
|
sessionId: string,
|
|
messageId: string,
|
|
completedAt: number
|
|
) => {
|
|
if (!sessionId || !messageId) {
|
|
return;
|
|
}
|
|
|
|
if (hasIndexedDbSupport()) {
|
|
try {
|
|
const db = await getDatabase();
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
const store = tx.objectStore(STORE_NAME);
|
|
store.put({ messageId, completedAt }, sessionId);
|
|
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error ?? new Error('Cursor write failed'));
|
|
tx.onabort = () => reject(tx.error ?? new Error('Cursor write aborted'));
|
|
});
|
|
return;
|
|
} catch { /* ignored */ }
|
|
}
|
|
|
|
const fallback = readFallback();
|
|
fallback[sessionId] = { messageId, completedAt };
|
|
writeFallback(fallback);
|
|
};
|
|
|
|
export const readSessionCursor = async (
|
|
sessionId: string
|
|
): Promise<CursorRecord | null> => {
|
|
if (!sessionId) {
|
|
return null;
|
|
}
|
|
|
|
if (hasIndexedDbSupport()) {
|
|
try {
|
|
const db = await getDatabase();
|
|
const record = await new Promise<CursorRecord | null>((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
const store = tx.objectStore(STORE_NAME);
|
|
const request = store.get(sessionId);
|
|
|
|
request.onsuccess = () => {
|
|
resolve((request.result as CursorRecord) ?? null);
|
|
};
|
|
|
|
request.onerror = () => reject(request.error ?? new Error('Cursor read failed'));
|
|
tx.onerror = () => reject(tx.error ?? new Error('Cursor read failed'));
|
|
tx.onabort = () => reject(tx.error ?? new Error('Cursor read aborted'));
|
|
});
|
|
return record;
|
|
} catch { /* ignored */ }
|
|
}
|
|
|
|
const fallback = readFallback();
|
|
return fallback[sessionId] ?? null;
|
|
};
|
|
|
|
export const clearSessionCursor = async (sessionId: string) => {
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
|
|
if (hasIndexedDbSupport()) {
|
|
try {
|
|
const db = await getDatabase();
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
const store = tx.objectStore(STORE_NAME);
|
|
store.delete(sessionId);
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error ?? new Error('Cursor delete failed'));
|
|
tx.onabort = () => reject(tx.error ?? new Error('Cursor delete aborted'));
|
|
});
|
|
} catch { /* ignored */ }
|
|
}
|
|
|
|
const fallback = readFallback();
|
|
if (sessionId in fallback) {
|
|
delete fallback[sessionId];
|
|
writeFallback(fallback);
|
|
}
|
|
};
|