diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index f96b0b99..5168a1be 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -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 = ({ return trimmedTitle && trimmedTitle.length > 0 ? trimmedTitle : 'Untitled Session'; }, [activeProjectLabel, currentSession?.title, currentSessionId]); - const currentSessionDiffStats = React.useMemo(() => { - return resolveSessionDiffStats(currentSession?.summary as Parameters[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 = ({ {isNewSessionDraftOpen ? t('sessions.switcher.draftTitle') : currentSessionTitle} - {(activeProjectLabel || currentBranchLabel || (!isNewSessionDraftOpen && (hasNonZeroSessionChanges || worktreeBadgeKind))) ? ( + {(activeProjectLabel || currentBranchLabel || (!isNewSessionDraftOpen && worktreeBadgeKind)) ? ( {activeProjectLabel ? {activeProjectLabel} : null} {currentBranchLabel ? ( @@ -2149,13 +2137,6 @@ export const Header: React.FC = ({ {currentBranchLabel} ) : null} - {!isNewSessionDraftOpen && hasNonZeroSessionChanges ? ( - - +{currentSessionChanges.additions} - / - -{currentSessionChanges.deletions} - - ) : null} {!isNewSessionDraftOpen && worktreeBadgeKind ? ( = ({ 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[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 }) => { {branchLabel} ) : null} - {hasChanges ? ( - - +{changes.additions} - / - -{changes.deletions} - - ) : null} diff --git a/packages/ui/src/components/session/SessionSwitcherDropdown.tsx b/packages/ui/src/components/session/SessionSwitcherDropdown.tsx index d48bda25..c07208b6 100644 --- a/packages/ui/src/components/session/SessionSwitcherDropdown.tsx +++ b/packages/ui/src/components/session/SessionSwitcherDropdown.tsx @@ -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 {branchLabel} ) : null} - {diffStats ? ( - - +{diffStats.additions} - / - -{diffStats.deletions} - - ) : null} ) : null} diff --git a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx index 2dee2656..f3c94e8c 100644 --- a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx +++ b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx @@ -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); diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index d84ec153..46344233 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -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 {
{hasChildren ? {isExpanded ? : } : null} {sessionUpdatedLabel} - {sessionDiffStats ? +{sessionDiffStats.additions}/-{sessionDiffStats.deletions} : null} {hasSecondaryProjectLabel ? {secondaryMeta?.projectLabel} : null} {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null}
@@ -992,14 +992,11 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { {secondaryMeta?.projectLabel ?
{secondaryMeta.projectLabel}
: null}
{sessionUpdatedLabel}
- {secondaryMeta?.branchLabel || sessionDiffStats ? ( -
- {secondaryMeta?.branchLabel ? ( -
- {secondaryMeta.branchLabel} -
- ) : null} - {sessionDiffStats ? +{sessionDiffStats.additions}-{sessionDiffStats.deletions} : null} + {secondaryMeta?.branchLabel ? ( +
+
+ {secondaryMeta.branchLabel} +
) : null}
@@ -1040,7 +1037,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
{sessionUpdatedLabel} - {sessionDiffStats ? +{sessionDiffStats.additions}/-{sessionDiffStats.deletions} : null} {hasSecondaryProjectLabel ? {secondaryMeta?.projectLabel} : null} {hasSecondaryBranchLabel ? {secondaryMeta?.branchLabel} : null}
diff --git a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx index e9bafb0c..b1d37f89 100644 --- a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx @@ -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 {

{t('sessions.sidebar.header.displayMode.label')}

- setDisplayMode('default')} - className="flex items-center justify-between" - > - {t('sessions.sidebar.header.displayMode.default')} - {displayMode === 'default' ? : null} - - setDisplayMode('minimal')} - className="flex items-center justify-between" - > - {t('sessions.sidebar.header.displayMode.minimal')} - {displayMode === 'minimal' ? : null} - + {showDisplayModeToggle ? ( + <> + setDisplayMode('default')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.displayMode.default')} + {displayMode === 'default' ? : null} + + setDisplayMode('minimal')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.displayMode.minimal')} + {displayMode === 'minimal' ? : null} + + + ) : null} {showRecentControls ? ( <> - + {showDisplayModeToggle ? : null} ; -}; - export type SessionNode = { session: Session; children: SessionNode[]; diff --git a/packages/ui/src/components/session/sidebar/utils.tsx b/packages/ui/src/components/session/sidebar/utils.tsx index 2e7907c7..47c88ce0 100644 --- a/packages/ui/src/components/session/sidebar/utils.tsx +++ b/packages/ui/src/components/session/sidebar/utils.tsx @@ -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[1], params?: Parameters[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 diff --git a/packages/ui/src/stores/useSessionDisplayStore.ts b/packages/ui/src/stores/useSessionDisplayStore.ts index d3fd0294..36a02102 100644 --- a/packages/ui/src/stores/useSessionDisplayStore.ts +++ b/packages/ui/src/stores/useSessionDisplayStore.ts @@ -17,7 +17,7 @@ type SessionDisplayStore = { export const useSessionDisplayStore = create()( persist( (set) => ({ - displayMode: 'default', + displayMode: 'minimal', showRecentSection: true, showArchivedSessions: true, setDisplayMode: (mode) => set({ displayMode: mode }), @@ -28,6 +28,17 @@ export const useSessionDisplayStore = create()( }), { 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; + if (version < 1) { + return { ...state, displayMode: 'minimal' }; + } + return state; + }, }, ), );