Files
openchamber/packages/ui/src/lib/messageCursorPersistence.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

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);
}
};