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
@@ -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