Merge pull request #11 from matt1398/feat/hide-sessions

feat(sessions): implement session hiding and un-hiding functionality
This commit is contained in:
matt 2026-02-16 21:40:58 +09:00 committed by GitHub
commit 21d4e1c98e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 686 additions and 52 deletions

View file

@ -17,10 +17,7 @@
* - config:testTrigger: Test a trigger against historical session data * - config:testTrigger: Test a trigger against historical session data
*/ */
import { import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';
getAutoDetectedClaudeBasePath,
getClaudeBasePath,
} from '@main/utils/pathDecoder';
import { getErrorMessage } from '@shared/utils/errorHandling'; import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
@ -54,9 +51,8 @@ const execFileAsync = promisify(execFile);
// Get singleton instance // Get singleton instance
const configManager = ConfigManager.getInstance(); const configManager = ConfigManager.getInstance();
let onClaudeRootPathUpdated: let onClaudeRootPathUpdated: ((claudeRootPath: string | null) => Promise<void> | void) | null =
| ((claudeRootPath: string | null) => Promise<void> | void) null;
| null = null;
/** /**
* Response type for config operations * Response type for config operations
@ -111,6 +107,12 @@ export function registerConfigHandlers(ipcMain: IpcMain): void {
ipcMain.handle('config:pinSession', handlePinSession); ipcMain.handle('config:pinSession', handlePinSession);
ipcMain.handle('config:unpinSession', handleUnpinSession); ipcMain.handle('config:unpinSession', handleUnpinSession);
// Session hide handlers
ipcMain.handle('config:hideSession', handleHideSession);
ipcMain.handle('config:unhideSession', handleUnhideSession);
ipcMain.handle('config:hideSessions', handleHideSessions);
ipcMain.handle('config:unhideSessions', handleUnhideSessions);
// Dialog handlers // Dialog handlers
ipcMain.handle('config:selectFolders', handleSelectFolders); ipcMain.handle('config:selectFolders', handleSelectFolders);
ipcMain.handle('config:selectClaudeRootFolder', handleSelectClaudeRootFolder); ipcMain.handle('config:selectClaudeRootFolder', handleSelectClaudeRootFolder);
@ -789,9 +791,10 @@ function decodeWslOutput(output: string | Buffer | undefined): string {
} }
const hasUtf16LeBom = output.length >= 2 && output[0] === 0xff && output[1] === 0xfe; const hasUtf16LeBom = output.length >= 2 && output[0] === 0xff && output[1] === 0xfe;
const decoded = hasUtf16LeBom || looksLikeUtf16Le(output) const decoded =
? output.toString('utf16le') hasUtf16LeBom || looksLikeUtf16Le(output)
: output.toString('utf8'); ? output.toString('utf16le')
: output.toString('utf8');
return decoded.replace(/\0/g, ''); return decoded.replace(/\0/g, '');
} }
@ -853,11 +856,7 @@ function parseWslDistros(stdout: string): string[] {
} }
async function listWslDistros(): Promise<string[]> { async function listWslDistros(): Promise<string[]> {
const commands: string[][] = [ const commands: string[][] = [['--list', '--quiet'], ['-l', '-q'], ['-l']];
['--list', '--quiet'],
['-l', '-q'],
['-l'],
];
for (const command of commands) { for (const command of commands) {
try { try {
@ -885,10 +884,7 @@ function stripDefaultSuffix(input: string): string {
async function resolveWslHome(distro: string): Promise<string | null> { async function resolveWslHome(distro: string): Promise<string | null> {
try { try {
const { stdout } = await runWsl( const { stdout } = await runWsl(['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'], 5000);
['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'],
5000
);
return normalizeWslHomePath(stdout); return normalizeWslHomePath(stdout);
} catch { } catch {
return null; return null;
@ -958,6 +954,102 @@ async function handleFindWslClaudeRoots(
} }
} }
/**
* Handler for 'config:hideSession' - Hides a session for a project.
*/
async function handleHideSession(
_event: IpcMainInvokeEvent,
projectId: string,
sessionId: string
): Promise<ConfigResult> {
try {
if (!projectId || typeof projectId !== 'string') {
return { success: false, error: 'Project ID is required and must be a string' };
}
if (!sessionId || typeof sessionId !== 'string') {
return { success: false, error: 'Session ID is required and must be a string' };
}
configManager.hideSession(projectId, sessionId);
return { success: true };
} catch (error) {
logger.error('Error in config:hideSession:', error);
return { success: false, error: getErrorMessage(error) };
}
}
/**
* Handler for 'config:unhideSession' - Unhides a session for a project.
*/
async function handleUnhideSession(
_event: IpcMainInvokeEvent,
projectId: string,
sessionId: string
): Promise<ConfigResult> {
try {
if (!projectId || typeof projectId !== 'string') {
return { success: false, error: 'Project ID is required and must be a string' };
}
if (!sessionId || typeof sessionId !== 'string') {
return { success: false, error: 'Session ID is required and must be a string' };
}
configManager.unhideSession(projectId, sessionId);
return { success: true };
} catch (error) {
logger.error('Error in config:unhideSession:', error);
return { success: false, error: getErrorMessage(error) };
}
}
/**
* Handler for 'config:hideSessions' - Bulk hide sessions for a project.
*/
async function handleHideSessions(
_event: IpcMainInvokeEvent,
projectId: string,
sessionIds: string[]
): Promise<ConfigResult> {
try {
if (!projectId || typeof projectId !== 'string') {
return { success: false, error: 'Project ID is required and must be a string' };
}
if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
return { success: false, error: 'Session IDs must be an array of strings' };
}
configManager.hideSessions(projectId, sessionIds);
return { success: true };
} catch (error) {
logger.error('Error in config:hideSessions:', error);
return { success: false, error: getErrorMessage(error) };
}
}
/**
* Handler for 'config:unhideSessions' - Bulk unhide sessions for a project.
*/
async function handleUnhideSessions(
_event: IpcMainInvokeEvent,
projectId: string,
sessionIds: string[]
): Promise<ConfigResult> {
try {
if (!projectId || typeof projectId !== 'string') {
return { success: false, error: 'Project ID is required and must be a string' };
}
if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
return { success: false, error: 'Session IDs must be an array of strings' };
}
configManager.unhideSessions(projectId, sessionIds);
return { success: true };
} catch (error) {
logger.error('Error in config:unhideSessions:', error);
return { success: false, error: getErrorMessage(error) };
}
}
// ============================================================================= // =============================================================================
// Cleanup // Cleanup
// ============================================================================= // =============================================================================
@ -982,6 +1074,10 @@ export function removeConfigHandlers(ipcMain: IpcMain): void {
ipcMain.removeHandler('config:testTrigger'); ipcMain.removeHandler('config:testTrigger');
ipcMain.removeHandler('config:pinSession'); ipcMain.removeHandler('config:pinSession');
ipcMain.removeHandler('config:unpinSession'); ipcMain.removeHandler('config:unpinSession');
ipcMain.removeHandler('config:hideSession');
ipcMain.removeHandler('config:unhideSession');
ipcMain.removeHandler('config:hideSessions');
ipcMain.removeHandler('config:unhideSessions');
ipcMain.removeHandler('config:selectFolders'); ipcMain.removeHandler('config:selectFolders');
ipcMain.removeHandler('config:selectClaudeRootFolder'); ipcMain.removeHandler('config:selectClaudeRootFolder');
ipcMain.removeHandler('config:getClaudeRootInfo'); ipcMain.removeHandler('config:getClaudeRootInfo');

View file

@ -191,6 +191,7 @@ export interface DisplayConfig {
export interface SessionsConfig { export interface SessionsConfig {
pinnedSessions: Record<string, { sessionId: string; pinnedAt: number }[]>; pinnedSessions: Record<string, { sessionId: string; pinnedAt: number }[]>;
hiddenSessions: Record<string, { sessionId: string; hiddenAt: number }[]>;
} }
export interface SshPersistConfig { export interface SshPersistConfig {
@ -255,6 +256,7 @@ const DEFAULT_CONFIG: AppConfig = {
}, },
sessions: { sessions: {
pinnedSessions: {}, pinnedSessions: {},
hiddenSessions: {},
}, },
ssh: { ssh: {
lastConnection: null, lastConnection: null,
@ -746,6 +748,89 @@ export class ConfigManager {
this.saveConfig(); this.saveConfig();
} }
// ===========================================================================
// Session Hide Management
// ===========================================================================
/**
* Hides a session for a project.
* @param projectId - The project ID
* @param sessionId - The session ID to hide
*/
hideSession(projectId: string, sessionId: string): void {
const hidden = this.config.sessions.hiddenSessions[projectId] ?? [];
if (hidden.some((h) => h.sessionId === sessionId)) {
return;
}
this.config.sessions.hiddenSessions[projectId] = [
{ sessionId, hiddenAt: Date.now() },
...hidden,
];
this.saveConfig();
}
/**
* Unhides a session for a project.
* @param projectId - The project ID
* @param sessionId - The session ID to unhide
*/
unhideSession(projectId: string, sessionId: string): void {
const hidden = this.config.sessions.hiddenSessions[projectId];
if (!hidden) return;
this.config.sessions.hiddenSessions[projectId] = hidden.filter(
(h) => h.sessionId !== sessionId
);
if (this.config.sessions.hiddenSessions[projectId].length === 0) {
delete this.config.sessions.hiddenSessions[projectId];
}
this.saveConfig();
}
/**
* Hides multiple sessions for a project in a single write.
* @param projectId - The project ID
* @param sessionIds - The session IDs to hide
*/
hideSessions(projectId: string, sessionIds: string[]): void {
const hidden = this.config.sessions.hiddenSessions[projectId] ?? [];
const existingIds = new Set(hidden.map((h) => h.sessionId));
const now = Date.now();
const newEntries = sessionIds
.filter((id) => !existingIds.has(id))
.map((sessionId) => ({ sessionId, hiddenAt: now }));
if (newEntries.length === 0) return;
this.config.sessions.hiddenSessions[projectId] = [...newEntries, ...hidden];
this.saveConfig();
}
/**
* Unhides multiple sessions for a project in a single write.
* @param projectId - The project ID
* @param sessionIds - The session IDs to unhide
*/
unhideSessions(projectId: string, sessionIds: string[]): void {
const hidden = this.config.sessions.hiddenSessions[projectId];
if (!hidden) return;
const toRemove = new Set(sessionIds);
this.config.sessions.hiddenSessions[projectId] = hidden.filter(
(h) => !toRemove.has(h.sessionId)
);
if (this.config.sessions.hiddenSessions[projectId].length === 0) {
delete this.config.sessions.hiddenSessions[projectId];
}
this.saveConfig();
}
// =========================================================================== // ===========================================================================
// SSH Profile Management // SSH Profile Management
// =========================================================================== // ===========================================================================

View file

@ -68,6 +68,18 @@ export const CONFIG_PIN_SESSION = 'config:pinSession';
/** Unpin a session */ /** Unpin a session */
export const CONFIG_UNPIN_SESSION = 'config:unpinSession'; export const CONFIG_UNPIN_SESSION = 'config:unpinSession';
/** Hide a session */
export const CONFIG_HIDE_SESSION = 'config:hideSession';
/** Unhide a session */
export const CONFIG_UNHIDE_SESSION = 'config:unhideSession';
/** Bulk hide sessions */
export const CONFIG_HIDE_SESSIONS = 'config:hideSessions';
/** Bulk unhide sessions */
export const CONFIG_UNHIDE_SESSIONS = 'config:unhideSessions';
// ============================================================================= // =============================================================================
// SSH API Channels // SSH API Channels
// ============================================================================= // =============================================================================

View file

@ -36,6 +36,8 @@ import {
CONFIG_GET, CONFIG_GET,
CONFIG_GET_CLAUDE_ROOT_INFO, CONFIG_GET_CLAUDE_ROOT_INFO,
CONFIG_GET_TRIGGERS, CONFIG_GET_TRIGGERS,
CONFIG_HIDE_SESSION,
CONFIG_HIDE_SESSIONS,
CONFIG_OPEN_IN_EDITOR, CONFIG_OPEN_IN_EDITOR,
CONFIG_PIN_SESSION, CONFIG_PIN_SESSION,
CONFIG_REMOVE_IGNORE_REGEX, CONFIG_REMOVE_IGNORE_REGEX,
@ -45,6 +47,8 @@ import {
CONFIG_SELECT_FOLDERS, CONFIG_SELECT_FOLDERS,
CONFIG_SNOOZE, CONFIG_SNOOZE,
CONFIG_TEST_TRIGGER, CONFIG_TEST_TRIGGER,
CONFIG_UNHIDE_SESSION,
CONFIG_UNHIDE_SESSIONS,
CONFIG_UNPIN_SESSION, CONFIG_UNPIN_SESSION,
CONFIG_UPDATE, CONFIG_UPDATE,
CONFIG_UPDATE_TRIGGER, CONFIG_UPDATE_TRIGGER,
@ -292,6 +296,18 @@ const electronAPI: ElectronAPI = {
unpinSession: async (projectId: string, sessionId: string): Promise<void> => { unpinSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNPIN_SESSION, projectId, sessionId); return invokeIpcWithResult<void>(CONFIG_UNPIN_SESSION, projectId, sessionId);
}, },
hideSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_HIDE_SESSION, projectId, sessionId);
},
unhideSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNHIDE_SESSION, projectId, sessionId);
},
hideSessions: async (projectId: string, sessionIds: string[]): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_HIDE_SESSIONS, projectId, sessionIds);
},
unhideSessions: async (projectId: string, sessionIds: string[]): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNHIDE_SESSIONS, projectId, sessionIds);
},
}, },
// Deep link navigation // Deep link navigation

View file

@ -436,6 +436,14 @@ export class HttpAPIClient implements ElectronAPI {
this.post('/api/config/pin-session', { projectId, sessionId }), this.post('/api/config/pin-session', { projectId, sessionId }),
unpinSession: (projectId: string, sessionId: string): Promise<void> => unpinSession: (projectId: string, sessionId: string): Promise<void> =>
this.post('/api/config/unpin-session', { projectId, sessionId }), this.post('/api/config/unpin-session', { projectId, sessionId }),
hideSession: (projectId: string, sessionId: string): Promise<void> =>
this.post('/api/config/hide-session', { projectId, sessionId }),
unhideSession: (projectId: string, sessionId: string): Promise<void> =>
this.post('/api/config/unhide-session', { projectId, sessionId }),
hideSessions: (projectId: string, sessionIds: string[]): Promise<void> =>
this.post('/api/config/hide-sessions', { projectId, sessionIds }),
unhideSessions: (projectId: string, sessionIds: string[]): Promise<void> =>
this.post('/api/config/unhide-sessions', { projectId, sessionIds }),
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -48,6 +48,8 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
splitPane, splitPane,
togglePinSession, togglePinSession,
pinnedSessionIds, pinnedSessionIds,
toggleHideSession,
hiddenSessionIds,
} = useStore( } = useStore(
useShallow((s) => ({ useShallow((s) => ({
pane: s.paneLayout.panes.find((p) => p.id === paneId), pane: s.paneLayout.panes.find((p) => p.id === paneId),
@ -72,6 +74,8 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
splitPane: s.splitPane, splitPane: s.splitPane,
togglePinSession: s.togglePinSession, togglePinSession: s.togglePinSession,
pinnedSessionIds: s.pinnedSessionIds, pinnedSessionIds: s.pinnedSessionIds,
toggleHideSession: s.toggleHideSession,
hiddenSessionIds: s.hiddenSessionIds,
})) }))
); );
@ -235,6 +239,10 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
isContextMenuTabSession && contextMenuTab?.sessionId isContextMenuTabSession && contextMenuTab?.sessionId
? pinnedSessionIds.includes(contextMenuTab.sessionId) ? pinnedSessionIds.includes(contextMenuTab.sessionId)
: false; : false;
const isContextMenuTabHidden =
isContextMenuTabSession && contextMenuTab?.sessionId
? hiddenSessionIds.includes(contextMenuTab.sessionId)
: false;
// Show sidebar expand button only in the leftmost pane // Show sidebar expand button only in the leftmost pane
const isLeftmostPane = useStore( const isLeftmostPane = useStore(
@ -427,6 +435,12 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
? () => togglePinSession(contextMenuTab.sessionId!) ? () => togglePinSession(contextMenuTab.sessionId!)
: undefined : undefined
} }
isHidden={isContextMenuTabHidden}
onToggleHide={
isContextMenuTabSession && contextMenuTab?.sessionId
? () => toggleHideSession(contextMenuTab.sessionId!)
: undefined
}
/> />
)} )}
</div> </div>

View file

@ -27,6 +27,10 @@ interface TabContextMenuProps {
isPinned?: boolean; isPinned?: boolean;
/** Callback to toggle pin state */ /** Callback to toggle pin state */
onTogglePin?: () => void; onTogglePin?: () => void;
/** Whether this session is currently hidden from the sidebar */
isHidden?: boolean;
/** Callback to toggle hide state */
onToggleHide?: () => void;
} }
export const TabContextMenu = ({ export const TabContextMenu = ({
@ -44,6 +48,8 @@ export const TabContextMenu = ({
isSessionTab, isSessionTab,
isPinned, isPinned,
onTogglePin, onTogglePin,
isHidden,
onToggleHide,
}: TabContextMenuProps): React.JSX.Element => { }: TabContextMenuProps): React.JSX.Element => {
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@ -114,6 +120,12 @@ export const TabContextMenu = ({
/> />
</> </>
)} )}
{isSessionTab && onToggleHide && (
<MenuItem
label={isHidden ? 'Unhide from Sidebar' : 'Hide from Sidebar'}
onClick={handleClick(onToggleHide)}
/>
)}
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} /> <div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem label="Close All Tabs" shortcut="⇧⌘W" onClick={handleClick(onCloseAllTabs)} /> <MenuItem label="Close All Tabs" shortcut="⇧⌘W" onClick={handleClick(onCloseAllTabs)} />
</div> </div>

View file

@ -295,6 +295,7 @@ export function useSettingsHandlers({
}, },
sessions: { sessions: {
pinnedSessions: {}, pinnedSessions: {},
hiddenSessions: {},
}, },
}; };

View file

@ -1,6 +1,7 @@
/** /**
* DateGroupedSessions - Sessions organized by date categories with virtual scrolling. * DateGroupedSessions - Sessions organized by date categories with virtual scrolling.
* Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll. * Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll.
* Supports multi-select with bulk actions and hidden session filtering.
*/ */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -13,7 +14,17 @@ import {
separatePinnedSessions, separatePinnedSessions,
} from '@renderer/utils/dateGrouping'; } from '@renderer/utils/dateGrouping';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { ArrowDownWideNarrow, Calendar, Loader2, MessageSquareOff, Pin } from 'lucide-react'; import {
ArrowDownWideNarrow,
Calendar,
CheckSquare,
Eye,
EyeOff,
Loader2,
MessageSquareOff,
Pin,
X,
} from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { SessionItem } from './SessionItem'; import { SessionItem } from './SessionItem';
@ -25,7 +36,7 @@ import type { DateCategory } from '@renderer/types/tabs';
type VirtualItem = type VirtualItem =
| { type: 'header'; category: DateCategory; id: string } | { type: 'header'; category: DateCategory; id: string }
| { type: 'pinned-header'; id: string } | { type: 'pinned-header'; id: string }
| { type: 'session'; session: Session; isPinned: boolean; id: string } | { type: 'session'; session: Session; isPinned: boolean; isHidden: boolean; id: string }
| { type: 'loader'; id: string }; | { type: 'loader'; id: string };
/** /**
@ -52,6 +63,17 @@ export const DateGroupedSessions = (): React.JSX.Element => {
pinnedSessionIds, pinnedSessionIds,
sessionSortMode, sessionSortMode,
setSessionSortMode, setSessionSortMode,
hiddenSessionIds,
showHiddenSessions,
toggleShowHiddenSessions,
sidebarSelectedSessionIds,
sidebarMultiSelectActive,
toggleSidebarSessionSelection,
clearSidebarSelection,
toggleSidebarMultiSelect,
hideMultipleSessions,
unhideMultipleSessions,
pinMultipleSessions,
} = useStore( } = useStore(
useShallow((s) => ({ useShallow((s) => ({
sessions: s.sessions, sessions: s.sessions,
@ -65,6 +87,17 @@ export const DateGroupedSessions = (): React.JSX.Element => {
pinnedSessionIds: s.pinnedSessionIds, pinnedSessionIds: s.pinnedSessionIds,
sessionSortMode: s.sessionSortMode, sessionSortMode: s.sessionSortMode,
setSessionSortMode: s.setSessionSortMode, setSessionSortMode: s.setSessionSortMode,
hiddenSessionIds: s.hiddenSessionIds,
showHiddenSessions: s.showHiddenSessions,
toggleShowHiddenSessions: s.toggleShowHiddenSessions,
sidebarSelectedSessionIds: s.sidebarSelectedSessionIds,
sidebarMultiSelectActive: s.sidebarMultiSelectActive,
toggleSidebarSessionSelection: s.toggleSidebarSessionSelection,
clearSidebarSelection: s.clearSidebarSelection,
toggleSidebarMultiSelect: s.toggleSidebarMultiSelect,
hideMultipleSessions: s.hideMultipleSessions,
unhideMultipleSessions: s.unhideMultipleSessions,
pinMultipleSessions: s.pinMultipleSessions,
})) }))
); );
@ -72,10 +105,19 @@ export const DateGroupedSessions = (): React.JSX.Element => {
const countRef = useRef<HTMLSpanElement>(null); const countRef = useRef<HTMLSpanElement>(null);
const [showCountTooltip, setShowCountTooltip] = useState(false); const [showCountTooltip, setShowCountTooltip] = useState(false);
const hiddenSet = useMemo(() => new Set(hiddenSessionIds), [hiddenSessionIds]);
const hasHiddenSessions = hiddenSessionIds.length > 0;
// Filter out hidden sessions unless showHiddenSessions is on
const visibleSessions = useMemo(() => {
if (showHiddenSessions) return sessions;
return sessions.filter((s) => !hiddenSet.has(s.id));
}, [sessions, hiddenSet, showHiddenSessions]);
// Separate pinned sessions from unpinned // Separate pinned sessions from unpinned
const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo( const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo(
() => separatePinnedSessions(sessions, pinnedSessionIds), () => separatePinnedSessions(visibleSessions, pinnedSessionIds),
[sessions, pinnedSessionIds] [visibleSessions, pinnedSessionIds]
); );
// Group only unpinned sessions by date // Group only unpinned sessions by date
@ -90,8 +132,10 @@ export const DateGroupedSessions = (): React.JSX.Element => {
// Sessions sorted by context consumption (for most-context sort mode) // Sessions sorted by context consumption (for most-context sort mode)
const contextSortedSessions = useMemo(() => { const contextSortedSessions = useMemo(() => {
if (sessionSortMode !== 'most-context') return []; if (sessionSortMode !== 'most-context') return [];
return [...sessions].sort((a, b) => (b.contextConsumption ?? 0) - (a.contextConsumption ?? 0)); return [...visibleSessions].sort(
}, [sessions, sessionSortMode]); (a, b) => (b.contextConsumption ?? 0) - (a.contextConsumption ?? 0)
);
}, [visibleSessions, sessionSortMode]);
// Flatten sessions with date headers into virtual list items // Flatten sessions with date headers into virtual list items
const virtualItems = useMemo((): VirtualItem[] => { const virtualItems = useMemo((): VirtualItem[] => {
@ -104,6 +148,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session', type: 'session',
session, session,
isPinned: pinnedSessionIds.includes(session.id), isPinned: pinnedSessionIds.includes(session.id),
isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`, id: `session-${session.id}`,
}); });
} }
@ -120,6 +165,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session', type: 'session',
session, session,
isPinned: true, isPinned: true,
isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`, id: `session-${session.id}`,
}); });
} }
@ -137,6 +183,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session', type: 'session',
session, session,
isPinned: false, isPinned: false,
isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`, id: `session-${session.id}`,
}); });
} }
@ -156,6 +203,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
sessionSortMode, sessionSortMode,
contextSortedSessions, contextSortedSessions,
pinnedSessionIds, pinnedSessionIds,
hiddenSet,
pinnedSessions, pinnedSessions,
nonEmptyCategories, nonEmptyCategories,
groupedSessions, groupedSessions,
@ -221,6 +269,32 @@ export const DateGroupedSessions = (): React.JSX.Element => {
fetchSessionsMore, fetchSessionsMore,
]); ]);
// Bulk action helpers
const selectedSet = useMemo(
() => new Set(sidebarSelectedSessionIds),
[sidebarSelectedSessionIds]
);
const someSelectedAreHidden = useMemo(
() => sidebarSelectedSessionIds.some((id) => hiddenSet.has(id)),
[sidebarSelectedSessionIds, hiddenSet]
);
const handleBulkHide = useCallback(() => {
void hideMultipleSessions(sidebarSelectedSessionIds);
clearSidebarSelection();
}, [hideMultipleSessions, sidebarSelectedSessionIds, clearSidebarSelection]);
const handleBulkUnhide = useCallback(() => {
const hiddenSelected = sidebarSelectedSessionIds.filter((id) => hiddenSet.has(id));
void unhideMultipleSessions(hiddenSelected);
clearSidebarSelection();
}, [unhideMultipleSessions, sidebarSelectedSessionIds, hiddenSet, clearSidebarSelection]);
const handleBulkPin = useCallback(() => {
void pinMultipleSessions(sidebarSelectedSessionIds);
clearSidebarSelection();
}, [pinMultipleSessions, sidebarSelectedSessionIds, clearSidebarSelection]);
if (!selectedProjectId) { if (!selectedProjectId) {
return ( return (
<div className="p-4"> <div className="p-4">
@ -337,19 +411,100 @@ export const DateGroupedSessions = (): React.JSX.Element => {
</div>, </div>,
document.body document.body
)} )}
<button <div className="ml-auto flex items-center gap-0.5">
onClick={() => {/* Multi-select toggle */}
setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent') <button
} onClick={toggleSidebarMultiSelect}
className="ml-auto rounded p-1 transition-colors hover:bg-white/5" className="rounded p-1 transition-colors hover:bg-white/5"
title={sessionSortMode === 'recent' ? 'Sort by context consumption' : 'Sort by recent'} title={sidebarMultiSelectActive ? 'Exit selection mode' : 'Select sessions'}
style={{
color: sidebarMultiSelectActive ? '#818cf8' : 'var(--color-text-muted)',
}}
>
<CheckSquare className="size-3.5" />
</button>
{/* Show hidden sessions toggle - only when hidden sessions exist */}
{hasHiddenSessions && (
<button
onClick={toggleShowHiddenSessions}
className="rounded p-1 transition-colors hover:bg-white/5"
title={showHiddenSessions ? 'Hide hidden sessions' : 'Show hidden sessions'}
style={{
color: showHiddenSessions ? '#818cf8' : 'var(--color-text-muted)',
}}
>
{showHiddenSessions ? <Eye className="size-3.5" /> : <EyeOff className="size-3.5" />}
</button>
)}
{/* Sort mode toggle */}
<button
onClick={() =>
setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
}
className="rounded p-1 transition-colors hover:bg-white/5"
title={sessionSortMode === 'recent' ? 'Sort by context consumption' : 'Sort by recent'}
style={{
color: sessionSortMode === 'most-context' ? '#818cf8' : 'var(--color-text-muted)',
}}
>
<ArrowDownWideNarrow className="size-3.5" />
</button>
</div>
</div>
{/* Bulk action bar - shown when sessions are selected */}
{sidebarMultiSelectActive && sidebarSelectedSessionIds.length > 0 && (
<div
className="flex items-center gap-1.5 border-b px-3 py-1.5"
style={{ style={{
color: sessionSortMode === 'most-context' ? '#818cf8' : 'var(--color-text-muted)', borderColor: 'var(--color-border)',
backgroundColor: 'var(--color-surface-raised)',
}} }}
> >
<ArrowDownWideNarrow className="size-3.5" /> <span
</button> className="text-[11px] font-medium"
</div> style={{ color: 'var(--color-text-secondary)' }}
>
{sidebarSelectedSessionIds.length} selected
</span>
<div className="ml-auto flex items-center gap-1">
<button
onClick={handleBulkPin}
className="rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors hover:bg-white/5"
style={{ color: 'var(--color-text-secondary)' }}
title="Pin selected sessions"
>
<Pin className="inline-block size-3" /> Pin
</button>
<button
onClick={handleBulkHide}
className="rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors hover:bg-white/5"
style={{ color: 'var(--color-text-secondary)' }}
title="Hide selected sessions"
>
<EyeOff className="inline-block size-3" /> Hide
</button>
{showHiddenSessions && someSelectedAreHidden && (
<button
onClick={handleBulkUnhide}
className="rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors hover:bg-white/5"
style={{ color: 'var(--color-text-secondary)' }}
title="Unhide selected sessions"
>
<Eye className="inline-block size-3" /> Unhide
</button>
)}
<button
onClick={clearSidebarSelection}
className="rounded p-0.5 transition-colors hover:bg-white/5"
style={{ color: 'var(--color-text-muted)' }}
title="Cancel selection"
>
<X className="size-3.5" />
</button>
</div>
</div>
)}
<div ref={parentRef} className="flex-1 overflow-y-auto"> <div ref={parentRef} className="flex-1 overflow-y-auto">
<div <div
@ -419,6 +574,10 @@ export const DateGroupedSessions = (): React.JSX.Element => {
session={item.session} session={item.session}
isActive={selectedSessionId === item.session.id} isActive={selectedSessionId === item.session.id}
isPinned={item.isPinned} isPinned={item.isPinned}
isHidden={item.isHidden}
multiSelectActive={sidebarMultiSelectActive}
isSelected={selectedSet.has(item.session.id)}
onToggleSelect={() => toggleSidebarSessionSelection(item.session.id)}
/> />
)} )}
</div> </div>

View file

@ -7,7 +7,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { MAX_PANES } from '@renderer/types/panes'; import { MAX_PANES } from '@renderer/types/panes';
import { Pin, PinOff } from 'lucide-react'; import { Eye, EyeOff, Pin, PinOff } from 'lucide-react';
interface SessionContextMenuProps { interface SessionContextMenuProps {
x: number; x: number;
@ -17,11 +17,13 @@ interface SessionContextMenuProps {
sessionLabel: string; sessionLabel: string;
paneCount: number; paneCount: number;
isPinned: boolean; isPinned: boolean;
isHidden: boolean;
onClose: () => void; onClose: () => void;
onOpenInCurrentPane: () => void; onOpenInCurrentPane: () => void;
onOpenInNewTab: () => void; onOpenInNewTab: () => void;
onSplitRightAndOpen: () => void; onSplitRightAndOpen: () => void;
onTogglePin: () => void; onTogglePin: () => void;
onToggleHide: () => void;
} }
export const SessionContextMenu = ({ export const SessionContextMenu = ({
@ -29,11 +31,13 @@ export const SessionContextMenu = ({
y, y,
paneCount, paneCount,
isPinned, isPinned,
isHidden,
onClose, onClose,
onOpenInCurrentPane, onOpenInCurrentPane,
onOpenInNewTab, onOpenInNewTab,
onSplitRightAndOpen, onSplitRightAndOpen,
onTogglePin, onTogglePin,
onToggleHide,
}: SessionContextMenuProps): React.JSX.Element => { }: SessionContextMenuProps): React.JSX.Element => {
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@ -55,7 +59,7 @@ export const SessionContextMenu = ({
}, [onClose]); }, [onClose]);
const menuWidth = 240; const menuWidth = 240;
const menuHeight = 180; const menuHeight = 204;
const clampedX = Math.min(x, window.innerWidth - menuWidth - 8); const clampedX = Math.min(x, window.innerWidth - menuWidth - 8);
const clampedY = Math.min(y, window.innerHeight - menuHeight - 8); const clampedY = Math.min(y, window.innerHeight - menuHeight - 8);
@ -92,6 +96,11 @@ export const SessionContextMenu = ({
icon={isPinned ? <PinOff className="size-4" /> : <Pin className="size-4" />} icon={isPinned ? <PinOff className="size-4" /> : <Pin className="size-4" />}
onClick={handleClick(onTogglePin)} onClick={handleClick(onTogglePin)}
/> />
<MenuItem
label={isHidden ? 'Unhide Session' : 'Hide Session'}
icon={isHidden ? <Eye className="size-4" /> : <EyeOff className="size-4" />}
onClick={handleClick(onToggleHide)}
/>
</div> </div>
); );
}; };

View file

@ -10,7 +10,7 @@ import { createPortal } from 'react-dom';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { formatTokensCompact } from '@shared/utils/tokenFormatting'; import { formatTokensCompact } from '@shared/utils/tokenFormatting';
import { formatDistanceToNowStrict } from 'date-fns'; import { formatDistanceToNowStrict } from 'date-fns';
import { MessageSquare, Pin } from 'lucide-react'; import { EyeOff, MessageSquare, Pin } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { OngoingIndicator } from '../common/OngoingIndicator'; import { OngoingIndicator } from '../common/OngoingIndicator';
@ -23,6 +23,10 @@ interface SessionItemProps {
session: Session; session: Session;
isActive?: boolean; isActive?: boolean;
isPinned?: boolean; isPinned?: boolean;
isHidden?: boolean;
multiSelectActive?: boolean;
isSelected?: boolean;
onToggleSelect?: () => void;
} }
/** /**
@ -112,7 +116,7 @@ const ConsumptionBadge = ({
<span className="tabular-nums">{formatTokensCompact(phase.contribution)}</span> <span className="tabular-nums">{formatTokensCompact(phase.contribution)}</span>
{phase.postCompaction != null && ( {phase.postCompaction != null && (
<span style={{ color: 'var(--color-text-muted)' }}> <span style={{ color: 'var(--color-text-muted)' }}>
(compacted {formatTokensCompact(phase.postCompaction)}) (compacted to {formatTokensCompact(phase.postCompaction)})
</span> </span>
)} )}
</div> </div>
@ -129,24 +133,42 @@ export const SessionItem = ({
session, session,
isActive, isActive,
isPinned, isPinned,
isHidden,
multiSelectActive,
isSelected,
onToggleSelect,
}: Readonly<SessionItemProps>): React.JSX.Element => { }: Readonly<SessionItemProps>): React.JSX.Element => {
const { openTab, activeProjectId, selectSession, paneCount, splitPane, togglePinSession } = const {
useStore( openTab,
useShallow((s) => ({ activeProjectId,
openTab: s.openTab, selectSession,
activeProjectId: s.activeProjectId, paneCount,
selectSession: s.selectSession, splitPane,
paneCount: s.paneLayout.panes.length, togglePinSession,
splitPane: s.splitPane, toggleHideSession,
togglePinSession: s.togglePinSession, } = useStore(
})) useShallow((s) => ({
); openTab: s.openTab,
activeProjectId: s.activeProjectId,
selectSession: s.selectSession,
paneCount: s.paneLayout.panes.length,
splitPane: s.splitPane,
togglePinSession: s.togglePinSession,
toggleHideSession: s.toggleHideSession,
}))
);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
const handleClick = (event: React.MouseEvent): void => { const handleClick = (event: React.MouseEvent): void => {
if (!activeProjectId) return; if (!activeProjectId) return;
// In multi-select mode, clicks toggle selection
if (multiSelectActive && onToggleSelect) {
onToggleSelect();
return;
}
// Cmd/Ctrl+click: open in new tab; plain click: replace current tab // Cmd/Ctrl+click: open in new tab; plain click: replace current tab
const forceNewTab = event.ctrlKey || event.metaKey; const forceNewTab = event.ctrlKey || event.metaKey;
@ -227,12 +249,23 @@ export const SessionItem = ({
style={{ style={{
borderColor: 'var(--color-border)', borderColor: 'var(--color-border)',
...(isActive ? { backgroundColor: 'var(--color-surface-raised)' } : {}), ...(isActive ? { backgroundColor: 'var(--color-surface-raised)' } : {}),
...(isHidden ? { opacity: 0.5 } : {}),
}} }}
> >
{/* First line: title + ongoing indicator + pin icon */} {/* First line: title + ongoing indicator + pin/hidden icons */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{multiSelectActive && (
<input
type="checkbox"
checked={isSelected ?? false}
onChange={() => onToggleSelect?.()}
onClick={(e) => e.stopPropagation()}
className="size-3.5 shrink-0 accent-blue-500"
/>
)}
{session.isOngoing && <OngoingIndicator />} {session.isOngoing && <OngoingIndicator />}
{isPinned && <Pin className="size-2.5 shrink-0 text-blue-400" />} {isPinned && <Pin className="size-2.5 shrink-0 text-blue-400" />}
{isHidden && <EyeOff className="size-2.5 shrink-0 text-zinc-500" />}
<span <span
className="truncate text-[13px] font-medium leading-tight" className="truncate text-[13px] font-medium leading-tight"
style={{ color: isActive ? 'var(--color-text)' : 'var(--color-text-muted)' }} style={{ color: isActive ? 'var(--color-text)' : 'var(--color-text-muted)' }}
@ -275,11 +308,13 @@ export const SessionItem = ({
sessionLabel={sessionLabel} sessionLabel={sessionLabel}
paneCount={paneCount} paneCount={paneCount}
isPinned={isPinned ?? false} isPinned={isPinned ?? false}
isHidden={isHidden ?? false}
onClose={() => setContextMenu(null)} onClose={() => setContextMenu(null)}
onOpenInCurrentPane={handleOpenInCurrentPane} onOpenInCurrentPane={handleOpenInCurrentPane}
onOpenInNewTab={handleOpenInNewTab} onOpenInNewTab={handleOpenInNewTab}
onSplitRightAndOpen={handleSplitRightAndOpen} onSplitRightAndOpen={handleSplitRightAndOpen}
onTogglePin={() => void togglePinSession(session.id)} onTogglePin={() => void togglePinSession(session.id)}
onToggleHide={() => void toggleHideSession(session.id)}
/>, />,
document.body document.body
)} )}

View file

@ -34,6 +34,12 @@ export interface SessionSlice {
sessionsLoadingMore: boolean; sessionsLoadingMore: boolean;
// Pinned sessions // Pinned sessions
pinnedSessionIds: string[]; pinnedSessionIds: string[];
// Hidden sessions
hiddenSessionIds: string[];
showHiddenSessions: boolean;
// Multi-select
sidebarSelectedSessionIds: string[];
sidebarMultiSelectActive: boolean;
// Sort mode // Sort mode
sessionSortMode: SessionSortMode; sessionSortMode: SessionSortMode;
@ -52,6 +58,24 @@ export interface SessionSlice {
loadPinnedSessions: () => Promise<void>; loadPinnedSessions: () => Promise<void>;
/** Set session sort mode */ /** Set session sort mode */
setSessionSortMode: (mode: SessionSortMode) => void; setSessionSortMode: (mode: SessionSortMode) => void;
/** Toggle hide/unhide for a session */
toggleHideSession: (sessionId: string) => Promise<void>;
/** Bulk hide sessions */
hideMultipleSessions: (sessionIds: string[]) => Promise<void>;
/** Bulk unhide sessions */
unhideMultipleSessions: (sessionIds: string[]) => Promise<void>;
/** Load hidden sessions from config for current project */
loadHiddenSessions: () => Promise<void>;
/** Toggle showing hidden sessions in sidebar */
toggleShowHiddenSessions: () => void;
/** Toggle one session's checkbox in sidebar multi-select */
toggleSidebarSessionSelection: (sessionId: string) => void;
/** Clear all selections and exit multi-select mode */
clearSidebarSelection: () => void;
/** Enter/exit selection mode */
toggleSidebarMultiSelect: () => void;
/** Bulk pin for multi-select */
pinMultipleSessions: (sessionIds: string[]) => Promise<void>;
} }
// ============================================================================= // =============================================================================
@ -71,6 +95,12 @@ export const createSessionSlice: StateCreator<AppState, [], [], SessionSlice> =
sessionsLoadingMore: false, sessionsLoadingMore: false,
// Pinned sessions // Pinned sessions
pinnedSessionIds: [], pinnedSessionIds: [],
// Hidden sessions
hiddenSessionIds: [],
showHiddenSessions: false,
// Multi-select
sidebarSelectedSessionIds: [],
sidebarMultiSelectActive: false,
// Sort mode // Sort mode
sessionSortMode: 'recent' as SessionSortMode, sessionSortMode: 'recent' as SessionSortMode,
@ -115,8 +145,9 @@ export const createSessionSlice: StateCreator<AppState, [], [], SessionSlice> =
sessionsLoading: false, sessionsLoading: false,
}); });
// Load pinned sessions after fetching session list // Load pinned and hidden sessions after fetching session list
void get().loadPinnedSessions(); void get().loadPinnedSessions();
void get().loadHiddenSessions();
} catch (error) { } catch (error) {
set({ set({
sessionsError: error instanceof Error ? error.message : 'Failed to fetch sessions', sessionsError: error instanceof Error ? error.message : 'Failed to fetch sessions',
@ -328,4 +359,150 @@ export const createSessionSlice: StateCreator<AppState, [], [], SessionSlice> =
setSessionSortMode: (mode: SessionSortMode) => { setSessionSortMode: (mode: SessionSortMode) => {
set({ sessionSortMode: mode }); set({ sessionSortMode: mode });
}, },
// Toggle hide/unhide for a session (optimistic update)
toggleHideSession: async (sessionId: string) => {
const state = get();
const projectId = state.selectedProjectId;
if (!projectId) return;
const isHidden = state.hiddenSessionIds.includes(sessionId);
const previousHiddenIds = state.hiddenSessionIds;
// Optimistic: update UI immediately
if (isHidden) {
set({ hiddenSessionIds: previousHiddenIds.filter((id) => id !== sessionId) });
} else {
set({ hiddenSessionIds: [sessionId, ...previousHiddenIds] });
}
try {
if (isHidden) {
await api.config.unhideSession(projectId, sessionId);
} else {
await api.config.hideSession(projectId, sessionId);
}
} catch (error) {
// Rollback on failure
set({ hiddenSessionIds: previousHiddenIds });
logger.error('toggleHideSession error:', error);
}
},
// Bulk hide sessions
hideMultipleSessions: async (sessionIds: string[]) => {
const state = get();
const projectId = state.selectedProjectId;
if (!projectId || sessionIds.length === 0) return;
const previousHiddenIds = state.hiddenSessionIds;
const existingSet = new Set(previousHiddenIds);
const newIds = sessionIds.filter((id) => !existingSet.has(id));
// Optimistic update
set({ hiddenSessionIds: [...newIds, ...previousHiddenIds] });
try {
await api.config.hideSessions(projectId, sessionIds);
} catch (error) {
set({ hiddenSessionIds: previousHiddenIds });
logger.error('hideMultipleSessions error:', error);
}
},
// Bulk unhide sessions
unhideMultipleSessions: async (sessionIds: string[]) => {
const state = get();
const projectId = state.selectedProjectId;
if (!projectId || sessionIds.length === 0) return;
const previousHiddenIds = state.hiddenSessionIds;
const toRemove = new Set(sessionIds);
// Optimistic update
set({ hiddenSessionIds: previousHiddenIds.filter((id) => !toRemove.has(id)) });
try {
await api.config.unhideSessions(projectId, sessionIds);
} catch (error) {
set({ hiddenSessionIds: previousHiddenIds });
logger.error('unhideMultipleSessions error:', error);
}
},
// Load hidden sessions from config for current project
loadHiddenSessions: async () => {
const state = get();
const projectId = state.selectedProjectId;
if (!projectId) {
set({ hiddenSessionIds: [] });
return;
}
try {
const config = await api.config.get();
const hidden = config.sessions?.hiddenSessions?.[projectId] ?? [];
const hiddenIds = hidden.map((h) => h.sessionId);
set({ hiddenSessionIds: hiddenIds });
} catch (error) {
logger.error('loadHiddenSessions error:', error);
set({ hiddenSessionIds: [] });
}
},
// Toggle showing hidden sessions in sidebar
toggleShowHiddenSessions: () => {
set((prev) => ({ showHiddenSessions: !prev.showHiddenSessions }));
},
// Toggle one session's checkbox in sidebar multi-select
toggleSidebarSessionSelection: (sessionId: string) => {
set((prev) => {
const selected = prev.sidebarSelectedSessionIds;
if (selected.includes(sessionId)) {
return { sidebarSelectedSessionIds: selected.filter((id) => id !== sessionId) };
}
return {
sidebarSelectedSessionIds: [...selected, sessionId],
sidebarMultiSelectActive: true,
};
});
},
// Clear all selections and exit multi-select mode
clearSidebarSelection: () => {
set({ sidebarSelectedSessionIds: [], sidebarMultiSelectActive: false });
},
// Enter/exit selection mode
toggleSidebarMultiSelect: () => {
set((prev) => {
if (prev.sidebarMultiSelectActive) {
return { sidebarMultiSelectActive: false, sidebarSelectedSessionIds: [] };
}
return { sidebarMultiSelectActive: true };
});
},
// Bulk pin for multi-select
pinMultipleSessions: async (sessionIds: string[]) => {
const state = get();
const projectId = state.selectedProjectId;
if (!projectId || sessionIds.length === 0) return;
const previousPinnedIds = state.pinnedSessionIds;
const existingSet = new Set(previousPinnedIds);
const newIds = sessionIds.filter((id) => !existingSet.has(id));
// Optimistic update
set({ pinnedSessionIds: [...newIds, ...previousPinnedIds] });
try {
// Pin each session individually (no bulk pin IPC)
await Promise.all(newIds.map((sessionId) => api.config.pinSession(projectId, sessionId)));
} catch (error) {
set({ pinnedSessionIds: previousPinnedIds });
logger.error('pinMultipleSessions error:', error);
}
},
}); });

View file

@ -99,6 +99,14 @@ export interface ConfigAPI {
pinSession: (projectId: string, sessionId: string) => Promise<void>; pinSession: (projectId: string, sessionId: string) => Promise<void>;
/** Unpin a session for a project */ /** Unpin a session for a project */
unpinSession: (projectId: string, sessionId: string) => Promise<void>; unpinSession: (projectId: string, sessionId: string) => Promise<void>;
/** Hide a session for a project */
hideSession: (projectId: string, sessionId: string) => Promise<void>;
/** Unhide a session for a project */
unhideSession: (projectId: string, sessionId: string) => Promise<void>;
/** Bulk hide sessions for a project */
hideSessions: (projectId: string, sessionIds: string[]) => Promise<void>;
/** Bulk unhide sessions for a project */
unhideSessions: (projectId: string, sessionIds: string[]) => Promise<void>;
} }
export interface ClaudeRootInfo { export interface ClaudeRootInfo {

View file

@ -276,6 +276,8 @@ export interface AppConfig {
sessions: { sessions: {
/** Pinned sessions per project. Key is projectId, value is array of pinned sessions */ /** Pinned sessions per project. Key is projectId, value is array of pinned sessions */
pinnedSessions: Record<string, { sessionId: string; pinnedAt: number }[]>; pinnedSessions: Record<string, { sessionId: string; pinnedAt: number }[]>;
/** Hidden sessions per project. Key is projectId, value is array of hidden sessions */
hiddenSessions: Record<string, { sessionId: string; hiddenAt: number }[]>;
}; };
/** SSH connection settings */ /** SSH connection settings */
ssh?: { ssh?: {