feat: add session switcher dropdown in header
Open recent sessions from chat headers Support session switching in mini chat Share pinned and active session state
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import {
|
||||
type ActiveNowEntry,
|
||||
addActiveNowSession,
|
||||
persistActiveNowEntries,
|
||||
pruneActiveNowEntries,
|
||||
readActiveNowEntries,
|
||||
} from '@/components/session/sidebar/activitySections';
|
||||
import { getSafeStorage } from './utils/safeStorage';
|
||||
|
||||
type ActiveNowStore = {
|
||||
entries: ActiveNowEntry[];
|
||||
setEntries: (entries: ActiveNowEntry[]) => void;
|
||||
addSession: (sessionId: string) => void;
|
||||
prune: (sessionsById: Map<string, Session>) => void;
|
||||
};
|
||||
|
||||
const safeStorage = getSafeStorage();
|
||||
|
||||
export const useActiveNowStore = create<ActiveNowStore>((set, get) => ({
|
||||
entries: readActiveNowEntries(safeStorage),
|
||||
setEntries: (entries) => {
|
||||
if (entries === get().entries) return;
|
||||
set({ entries });
|
||||
persistActiveNowEntries(safeStorage, entries);
|
||||
},
|
||||
addSession: (sessionId) => {
|
||||
const next = addActiveNowSession(get().entries, sessionId);
|
||||
if (next === get().entries) return;
|
||||
set({ entries: next });
|
||||
persistActiveNowEntries(safeStorage, next);
|
||||
},
|
||||
prune: (sessionsById) => {
|
||||
const current = get().entries;
|
||||
const pruned = pruneActiveNowEntries(current, sessionsById);
|
||||
if (
|
||||
pruned.length === current.length
|
||||
&& pruned.every((entry, index) => entry.sessionId === current[index]?.sessionId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
set({ entries: pruned });
|
||||
persistActiveNowEntries(safeStorage, pruned);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,54 @@
|
||||
import { create } from 'zustand';
|
||||
import { getSafeStorage } from './utils/safeStorage';
|
||||
|
||||
const SESSION_PINNED_STORAGE_KEY = 'oc.sessions.pinned';
|
||||
|
||||
const readPinned = (storage: Storage): Set<string> => {
|
||||
try {
|
||||
const raw = storage.getItem(SESSION_PINNED_STORAGE_KEY);
|
||||
if (!raw) return new Set();
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return new Set();
|
||||
return new Set(parsed.filter((item): item is string => typeof item === 'string'));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
};
|
||||
|
||||
const persistPinned = (storage: Storage, ids: Set<string>): void => {
|
||||
try {
|
||||
storage.setItem(SESSION_PINNED_STORAGE_KEY, JSON.stringify([...ids]));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
type SessionPinnedStore = {
|
||||
ids: Set<string>;
|
||||
setIds: (next: Set<string> | ((prev: Set<string>) => Set<string>)) => void;
|
||||
toggle: (sessionId: string) => void;
|
||||
};
|
||||
|
||||
const safeStorage = getSafeStorage();
|
||||
|
||||
export const useSessionPinnedStore = create<SessionPinnedStore>((set, get) => ({
|
||||
ids: readPinned(safeStorage),
|
||||
setIds: (next) => {
|
||||
const current = get().ids;
|
||||
const resolved = typeof next === 'function' ? next(current) : next;
|
||||
if (resolved === current) return;
|
||||
set({ ids: resolved });
|
||||
persistPinned(safeStorage, resolved);
|
||||
},
|
||||
toggle: (sessionId) => {
|
||||
const current = get().ids;
|
||||
const next = new Set(current);
|
||||
if (next.has(sessionId)) {
|
||||
next.delete(sessionId);
|
||||
} else {
|
||||
next.add(sessionId);
|
||||
}
|
||||
set({ ids: next });
|
||||
persistPinned(safeStorage, next);
|
||||
},
|
||||
}));
|
||||
@@ -492,6 +492,7 @@ interface UIStore {
|
||||
bottomTerminalHeight: number;
|
||||
hasManuallyResizedBottomTerminal: boolean;
|
||||
isSessionSwitcherOpen: boolean;
|
||||
isSessionDropdownOpen: boolean;
|
||||
activeMainTab: MainTab;
|
||||
mainTabGuard: MainTabGuard | null;
|
||||
sidebarOpenBeforeFullscreenTab: boolean | null;
|
||||
@@ -619,6 +620,7 @@ interface UIStore {
|
||||
setBottomTerminalExpanded: (expanded: boolean) => void;
|
||||
setBottomTerminalHeight: (height: number) => void;
|
||||
setSessionSwitcherOpen: (open: boolean) => void;
|
||||
setSessionDropdownOpen: (open: boolean) => void;
|
||||
setActiveMainTab: (tab: MainTab) => void;
|
||||
setMainTabGuard: (guard: MainTabGuard | null) => void;
|
||||
setPendingDiffFile: (filePath: string | null) => void;
|
||||
@@ -749,6 +751,7 @@ export const useUIStore = create<UIStore>()(
|
||||
bottomTerminalHeight: 300,
|
||||
hasManuallyResizedBottomTerminal: false,
|
||||
isSessionSwitcherOpen: false,
|
||||
isSessionDropdownOpen: false,
|
||||
activeMainTab: 'chat',
|
||||
mainTabGuard: null,
|
||||
sidebarOpenBeforeFullscreenTab: null,
|
||||
@@ -1279,6 +1282,10 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ isSessionSwitcherOpen: open });
|
||||
},
|
||||
|
||||
setSessionDropdownOpen: (open) => {
|
||||
set({ isSessionDropdownOpen: open });
|
||||
},
|
||||
|
||||
setMainTabGuard: (guard) => {
|
||||
if (get().mainTabGuard === guard) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user