refactor: simplify session list display

Makes minimal session rows the default
Removes session diff stat badges from navigation surfaces
Keeps expanded session rows in VS Code
This commit is contained in:
Bohdan Triapitsyn
2026-06-12 12:38:31 +03:00
parent c703db2745
commit 28aeb4950b
9 changed files with 51 additions and 125 deletions
+1 -20
View File
@@ -69,7 +69,6 @@ import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton';
import { SessionSwitcherDropdown } from '@/components/session/SessionSwitcherDropdown';
import { canUseElectronDesktopIPC, invokeDesktop, isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag, type UpdateInfo } from '@/lib/desktop';
import { desktopHostsGet, getDesktopHostApiUrl, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
@@ -1289,17 +1288,6 @@ export const Header: React.FC<HeaderProps> = ({
return trimmedTitle && trimmedTitle.length > 0 ? trimmedTitle : 'Untitled Session';
}, [activeProjectLabel, currentSession?.title, currentSessionId]);
const currentSessionDiffStats = React.useMemo(() => {
return resolveSessionDiffStats(currentSession?.summary as Parameters<typeof resolveSessionDiffStats>[0]);
}, [currentSession?.summary]);
const currentSessionChanges = React.useMemo(() => {
if (currentSessionDiffStats) {
return currentSessionDiffStats;
}
return { additions: 0, deletions: 0 };
}, [currentSessionDiffStats]);
const hasNonZeroSessionChanges = currentSessionChanges.additions > 0 || currentSessionChanges.deletions > 0;
const actionDirectory = React.useMemo(() => {
return normalize(openDirectory || activeProject?.path || '');
@@ -2140,7 +2128,7 @@ export const Header: React.FC<HeaderProps> = ({
<span className="truncate typography-ui-label text-[14px] font-normal leading-tight text-foreground max-w-full">
{isNewSessionDraftOpen ? t('sessions.switcher.draftTitle') : currentSessionTitle}
</span>
{(activeProjectLabel || currentBranchLabel || (!isNewSessionDraftOpen && (hasNonZeroSessionChanges || worktreeBadgeKind))) ? (
{(activeProjectLabel || currentBranchLabel || (!isNewSessionDraftOpen && worktreeBadgeKind)) ? (
<span className="flex min-w-0 max-w-full items-center gap-1.5 truncate typography-micro text-[10.5px] font-normal leading-tight text-muted-foreground/75">
{activeProjectLabel ? <span className="truncate">{activeProjectLabel}</span> : null}
{currentBranchLabel ? (
@@ -2149,13 +2137,6 @@ export const Header: React.FC<HeaderProps> = ({
<span className="truncate">{currentBranchLabel}</span>
</span>
) : null}
{!isNewSessionDraftOpen && hasNonZeroSessionChanges ? (
<span className="inline-flex flex-shrink-0 items-center gap-0 text-[0.92em]">
<span className="text-status-success/80">+{currentSessionChanges.additions}</span>
<span className="text-muted-foreground/60">/</span>
<span className="text-status-error/65">-{currentSessionChanges.deletions}</span>
</span>
) : null}
{!isNewSessionDraftOpen && worktreeBadgeKind ? (
<span className={cn(
"inline-flex min-w-0 items-center gap-0.5",
@@ -15,7 +15,6 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useGitBranchLabel, useGitStore } from '@/stores/useGitStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
import { Icon } from "@/components/icon/Icon";
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
@@ -136,11 +135,6 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
const gitBranchForDirectory = useGitBranchLabel(openDirectory || null);
const rawBranchLabel = gitBranchForDirectory || worktreeMetadataBranch || sessionWorktreeMetadata?.branch?.trim() || worktreeAttachment?.branch?.trim() || catalogWorktreeBranch;
const branchLabel = rawBranchLabel && rawBranchLabel !== 'HEAD' ? rawBranchLabel : null;
const diffStats = React.useMemo(() => {
return resolveSessionDiffStats(session?.summary as Parameters<typeof resolveSessionDiffStats>[0]);
}, [session?.summary]);
const changes = diffStats ?? { additions: 0, deletions: 0 };
const hasChanges = changes.additions > 0 || changes.deletions > 0;
const currentModel = getCurrentModel();
const latestAssistantModel = React.useMemo(() => {
for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) {
@@ -284,13 +278,6 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
<span className="truncate">{branchLabel}</span>
</span>
) : null}
{hasChanges ? (
<span className="inline-flex flex-shrink-0 items-center gap-0 text-[0.92em]">
<span className="text-status-success/80">+{changes.additions}</span>
<span className="text-muted-foreground/60">/</span>
<span className="text-status-error/65">-{changes.deletions}</span>
</span>
) : null}
</span>
</button>
</SessionSwitcherDropdown>
@@ -14,8 +14,8 @@ import { useSessionUnseenCount } from '@/sync/notification-store';
import { useSwitcherItems, type SwitcherItem } from '@/components/session/sidebar/hooks/useSwitcherItems';
import { useUIStore } from '@/stores/useUIStore';
import { resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
import { formatSessionCompactDateLabel, resolveSessionDiffStats } from './sidebar/utils';
import type { SessionNode, SessionSummaryMeta } from './sidebar/types';
import { formatSessionCompactDateLabel } from './sidebar/utils';
import type { SessionNode } from './sidebar/types';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
@@ -202,8 +202,6 @@ function SwitcherRow({ session, depth, variant, secondaryMeta, hasChildren, isEx
const isStreaming = statusType === 'busy' || statusType === 'retry';
const showUnreadDot = !isStreaming && needsAttention && !isActive;
const summary = session.summary as SessionSummaryMeta | undefined;
const diffStats = resolveSessionDiffStats(summary);
const timestamp = session.time?.updated || session.time?.created || Date.now();
const timeLabel = formatSessionCompactDateLabel(timestamp);
@@ -288,13 +286,6 @@ function SwitcherRow({ session, depth, variant, secondaryMeta, hasChildren, isEx
<span className="truncate">{branchLabel}</span>
</span>
) : null}
{diffStats ? (
<span className="inline-flex flex-shrink-0 items-center gap-0 text-[0.92em]">
<span className="text-status-success/80">+{diffStats.additions}</span>
<span className="text-muted-foreground/60">/</span>
<span className="text-status-error/65">-{diffStats.deletions}</span>
</span>
) : null}
</div>
) : null}
</div>
@@ -22,6 +22,7 @@ import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { openExternalUrl } from '@/lib/url';
import { isVSCodeRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
type DeleteFolderConfirm = {
@@ -157,7 +158,8 @@ export function SessionGroupSection(props: Props): React.ReactNode {
const searchData = hasSessionSearchQuery ? groupSearchDataByGroup.get(group) : null;
const displayMode = useSessionDisplayStore((state) => state.displayMode);
const foldersMap = useSessionFoldersStore((state) => state.foldersMap);
const isMinimalMode = displayMode === 'minimal';
// VS Code always uses the expanded layout (see SessionNodeItem).
const isMinimalMode = displayMode === 'minimal' && !isVSCodeRuntime();
const isCollapsed = hasSessionSearchQuery ? false : collapsedGroups.has(groupKey);
const maxVisible = hideDirectoryControls ? 10 : 5;
const nonArchivedVisibleCount = Math.max(maxVisible, visibleSessionCount ?? maxVisible);
@@ -25,8 +25,8 @@ import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSession
import { useSync } from '@/sync/use-sync';
import { useViewportStore, viewportSessionKey } from '@/sync/viewport-store';
import { DraggableSessionRow } from './sessionFolderDnd';
import type { SessionNode, SessionSummaryMeta } from './types';
import { formatSessionCompactDateLabel, formatSessionDateLabel, normalizePath, renderHighlightedText, resolveSessionDiffStats } from './utils';
import type { SessionNode } from './types';
import { formatSessionCompactDateLabel, formatSessionDateLabel, normalizePath, renderHighlightedText } from './utils';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useSessionUnseenCount } from '@/sync/notification-store';
import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore';
@@ -263,8 +263,11 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const hasSecondaryBranchLabel = Boolean(secondaryMeta?.branchLabel);
const displayMode = useSessionDisplayStore((state) => state.displayMode);
const isMinimalMode = displayMode === 'minimal';
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
// VS Code keeps the expanded "default" layout regardless of the stored mode:
// multi-workspace lists rely on inline project/branch, and hover tooltips
// across the whole list would be impractical there.
const isMinimalMode = displayMode === 'minimal' && !isVSCode;
const isElectron = React.useMemo(() => canUseElectronDesktopIPC(), []);
const runtimeApis = React.useContext(RuntimeAPIContext);
const revealOnHoverClass = isVSCode
@@ -348,8 +351,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const isSubtaskSession = Boolean((resolvedSession as Session & { parentID?: string | null }).parentID);
const unseenCount = useSessionUnseenCount(session.id);
const needsAttention = unseenCount > 0 && (!isSubtaskSession || notifyOnSubtasks);
const sessionSummary = resolvedSession.summary as SessionSummaryMeta | undefined;
const sessionDiffStats = resolveSessionDiffStats(sessionSummary);
const sessionTimestamp = resolvedSession.time?.updated || resolvedSession.time?.created || Date.now();
const sessionUpdatedLabel = formatSessionDateLabel(sessionTimestamp);
const sessionCompactUpdatedLabel = formatSessionCompactDateLabel(sessionTimestamp);
@@ -529,7 +530,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
{hasChildren ? <span className="inline-flex items-center justify-center flex-shrink-0">{isExpanded ? <Icon name="arrow-down-s" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}</span> : null}
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-status-error/65">/-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
@@ -992,14 +992,11 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
{secondaryMeta?.projectLabel ? <div className="min-w-0 truncate">{secondaryMeta.projectLabel}</div> : null}
<div className="flex-shrink-0">{sessionUpdatedLabel}</div>
</div>
{secondaryMeta?.branchLabel || sessionDiffStats ? (
<div className={cn('flex items-center gap-3 text-left text-muted-foreground', secondaryMeta?.branchLabel ? 'justify-between' : 'justify-start')}>
{secondaryMeta?.branchLabel ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
</div>
) : null}
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0.5"><span className="text-status-success">+{sessionDiffStats.additions}</span><span className="text-status-error">-{sessionDiffStats.deletions}</span></span> : null}
{secondaryMeta?.branchLabel ? (
<div className="flex items-center gap-3 text-left text-muted-foreground justify-start">
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
</div>
</div>
) : null}
</div>
@@ -1040,7 +1037,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className="flex items-center justify-between gap-3 text-muted-foreground/60 min-w-0 overflow-hidden leading-tight" style={{ fontSize: 'calc(var(--text-ui-label) * 0.85)' }}>
<div className={cn('flex min-w-0 items-center gap-1.5 overflow-hidden', metadataSubsessionChevron && hasChildren ? 'pl-4' : '')}>
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-muted-foreground/60">/</span><span className="text-status-error/65">-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
@@ -11,6 +11,7 @@ import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { Icon } from "@/components/icon/Icon";
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
type Props = {
@@ -67,6 +68,8 @@ export function SidebarHeader(props: Props): React.ReactNode {
const setDisplayMode = useSessionDisplayStore((state) => state.setDisplayMode);
const toggleRecentSection = useSessionDisplayStore((state) => state.toggleRecentSection);
const toggleArchivedSessions = useSessionDisplayStore((state) => state.toggleArchivedSessions);
// VS Code forces the expanded layout, so the mode toggle is meaningless there.
const showDisplayModeToggle = !isVSCodeRuntime();
if (hideDirectoryControls) {
return null;
@@ -188,23 +191,27 @@ export function SidebarHeader(props: Props): React.ReactNode {
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.displayMode.label')}</p></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[160px]">
<DropdownMenuItem
onClick={() => setDisplayMode('default')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
{showDisplayModeToggle ? (
<>
<DropdownMenuItem
onClick={() => setDisplayMode('default')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
</>
) : null}
{showRecentControls ? (
<>
<DropdownMenuSeparator />
{showDisplayModeToggle ? <DropdownMenuSeparator /> : null}
<DropdownMenuItem
onClick={toggleRecentSection}
className="flex items-center justify-between"
@@ -1,13 +1,6 @@
import type { Session } from '@opencode-ai/sdk/v2';
import type { WorktreeMetadata } from '@/types/worktree';
export type SessionSummaryMeta = {
additions?: number | string | null;
deletions?: number | string | null;
files?: number | null;
diffs?: Array<{ additions?: number | string | null; deletions?: number | string | null }>;
};
export type SessionNode = {
session: Session;
children: SessionNode[];
@@ -2,7 +2,6 @@ import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import { getCurrentIntlLocale } from '@/lib/i18n';
import { formatMessage, useI18nStore } from '@/lib/i18n/store';
import type { SessionSummaryMeta } from './types';
const t = (key: Parameters<typeof formatMessage>[1], params?: Parameters<typeof formatMessage>[2]) =>
formatMessage(useI18nStore.getState().dictionary, key, params);
@@ -209,47 +208,6 @@ export const isSessionRelatedToProject = (
return sessionDirectory === projectRoot || sessionDirectory.startsWith(`${projectRoot}/`);
};
const parseSummaryCount = (value: number | string | null | undefined): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
};
export const resolveSessionDiffStats = (summary?: SessionSummaryMeta): { additions: number; deletions: number } | null => {
if (!summary) {
return null;
}
const directAdditions = parseSummaryCount(summary.additions);
const directDeletions = parseSummaryCount(summary.deletions);
if (directAdditions !== null || directDeletions !== null) {
const stats = {
additions: Math.max(0, directAdditions ?? 0),
deletions: Math.max(0, directDeletions ?? 0),
};
return stats.additions === 0 && stats.deletions === 0 ? null : stats;
}
const diffs = Array.isArray(summary.diffs) ? summary.diffs : [];
if (diffs.length === 0) {
return null;
}
let additions = 0;
let deletions = 0;
diffs.forEach((diff) => {
additions += Math.max(0, parseSummaryCount(diff.additions) ?? 0);
deletions += Math.max(0, parseSummaryCount(diff.deletions) ?? 0);
});
return additions === 0 && deletions === 0 ? null : { additions, deletions };
};
export const formatProjectLabel = (label: string): string => {
return label
@@ -17,7 +17,7 @@ type SessionDisplayStore = {
export const useSessionDisplayStore = create<SessionDisplayStore>()(
persist(
(set) => ({
displayMode: 'default',
displayMode: 'minimal',
showRecentSection: true,
showArchivedSessions: true,
setDisplayMode: (mode) => set({ displayMode: mode }),
@@ -28,6 +28,17 @@ export const useSessionDisplayStore = create<SessionDisplayStore>()(
}),
{
name: 'session-display-mode',
version: 1,
// v0 shipped 'default' as the only/initial mode, so most existing users
// have it persisted by accident rather than choice. Nudge everyone onto
// minimal once so the mode can be evaluated before removing it entirely.
migrate: (persisted, version) => {
const state = (persisted ?? {}) as Partial<SessionDisplayStore>;
if (version < 1) {
return { ...state, displayMode: 'minimal' };
}
return state;
},
},
),
);