feat: Add branch name under worktree in session list (if different than worktree name) (#352)

* feat: show branch info in session sidebar and enrich worktrees

* feat: refresh sidebar branch display when switching branches in git menu

* fix: ensure branch name displays on mobile by fixing flex alignment

* style: make branch name font smaller, especially on mobile
This commit is contained in:
gsxdsm
2026-02-09 02:51:05 +02:00
committed by GitHub
parent 681918a089
commit 3f29b2c6a2
3 changed files with 68 additions and 13 deletions
@@ -68,6 +68,7 @@ import { checkIsGitRepository } from '@/lib/gitApi';
import { getSafeStorage } from '@/stores/utils/safeStorage';
import { createWorktreeOnly, createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
import { useGitStore } from '@/stores/useGitStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { updateDesktopSettings } from '@/lib/persistence';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
@@ -123,6 +124,19 @@ const normalizePath = (value?: string | null) => {
return normalized.length === 0 ? '/' : normalized;
};
const normalizeForBranchComparison = (value: string): string => {
return value
.toLowerCase()
.replace(/^opencode[/-]?/i, '')
.replace(/[-_]/g, '')
.trim();
};
const isBranchDifferentFromLabel = (branch: string | null, label: string): boolean => {
if (!branch) return false;
return normalizeForBranchComparison(branch) !== normalizeForBranchComparison(label);
};
const toFiniteNumber = (value: unknown): number | undefined => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
@@ -217,6 +231,7 @@ type SessionNode = {
type SessionGroup = {
id: string;
label: string;
branch: string | null;
description: string | null;
isMain: boolean;
worktree: WorktreeMetadata | null;
@@ -671,6 +686,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
const gitDirectories = useGitStore((state) => state.directories);
const sessions = useSessionStore((state) => state.sessions);
const sessionsByDirectory = useSessionStore((state) => state.sessionsByDirectory);
const currentSessionId = useSessionStore((state) => state.currentSessionId);
@@ -1241,6 +1258,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
label: (projectIsRepo && projectRootBranch && projectRootBranch !== 'HEAD')
? `project root: ${projectRootBranch}`
: 'project root',
branch: projectRootBranch ?? null,
description: normalizedProjectRoot ? formatPathForDisplay(normalizedProjectRoot, homeDirectory) : null,
isMain: true,
worktree: null,
@@ -1256,10 +1274,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
sortedWorktrees.forEach((meta) => {
const directory = normalizePath(meta.path) ?? meta.path;
const label = meta.branch || meta.name || meta.label || formatDirectoryName(directory, homeDirectory) || directory;
const label = meta.label || meta.name || formatDirectoryName(directory, homeDirectory) || directory;
groups.push({
id: `worktree:${directory}`,
label,
branch: meta.branch || null,
description: formatPathForDisplay(directory, homeDirectory),
isMain: false,
worktree: meta,
@@ -1277,6 +1296,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
groups.push({
id: `worktree:orphan:${directory}`,
label: formatDirectoryName(directory, homeDirectory) || directory,
branch: null,
description: formatPathForDisplay(directory, homeDirectory),
isMain: false,
worktree: null,
@@ -1364,6 +1384,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}>;
}, [projects]);
// Compute a dependency that changes when any project's git branch changes
const projectGitBranchesKey = React.useMemo(() => {
return normalizedProjects
.map((project) => {
const dirState = gitDirectories.get(project.normalizedPath);
return `${project.id}:${dirState?.status?.current ?? ''}`;
})
.join('|');
}, [normalizedProjects, gitDirectories]);
React.useEffect(() => {
let cancelled = false;
const run = async () => {
@@ -1390,7 +1420,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
return () => {
cancelled = true;
};
}, [normalizedProjects]);
}, [normalizedProjects, projectGitBranchesKey]);
const getSessionsForProject = React.useCallback(
(project: { normalizedPath: string }) => {
@@ -2263,7 +2293,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
return (
<div className="oc-group">
<div
className="group/gh flex items-center justify-between gap-2 py-1 min-h-8 min-w-0 rounded-sm hover:bg-interactive-hover/50 cursor-pointer"
className="group/gh flex items-start justify-between gap-2 py-1 min-w-0 rounded-sm hover:bg-interactive-hover/50 cursor-pointer"
onMouseEnter={() => {
if (!group.isMain) {
void ensureWorktreePrLoaded(groupKey, group.directory, group.label, group.worktree);
@@ -2304,11 +2334,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}}
aria-label={isCollapsed ? `Expand ${group.label}` : `Collapse ${group.label}`}
>
<div className="min-w-0 flex items-center gap-1.5 px-0">
<div className="min-w-0 flex items-start gap-1.5 px-0 pt-0.5">
{isCollapsed ? (
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground mt-1" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground mt-1" />
)}
{!group.isMain || isGitProject ? (
!group.isMain && groupPr?.url ? (
@@ -2322,7 +2352,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
void openExternal(groupPr.url);
}
}}
className="inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-interactive-hover/50"
className="inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-interactive-hover/50 mt-1"
style={{ color: prColorVar }}
aria-label={getPrTooltipLabel(groupPrStatus)}
>
@@ -2334,12 +2364,19 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
</TooltipContent>
</Tooltip>
) : (
<RiGitBranchLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<RiGitBranchLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground mt-1" />
)
) : null}
<p className={cn('text-[15px] font-semibold truncate', isActiveGroup ? 'text-primary' : 'text-muted-foreground')}>
{group.label}
</p>
<div className="min-w-0 flex flex-col">
<p className={cn('text-[15px] font-semibold truncate', isActiveGroup ? 'text-primary' : 'text-muted-foreground')}>
{group.label}
</p>
{!group.isMain && isBranchDifferentFromLabel(group.branch, group.label) ? (
<span className="text-[10px] sm:text-[11px] text-muted-foreground/80 truncate leading-tight">
{group.branch}
</span>
) : null}
</div>
</div>
{group.directory ? (
<div className="flex items-center gap-1 px-0.5">
@@ -1891,7 +1891,10 @@ export const FilesView: React.FC = () => {
value={draftContent}
onChange={setDraftContent}
extensions={editorExtensions}
className="h-full"
className={cn(
"h-full",
isMobile && "[&_.cm-scroller]:pb-[var(--oc-keyboard-inset,0px)]"
)}
highlightLines={lineSelection
? {
start: Math.min(lineSelection.start, lineSelection.end),
@@ -1,7 +1,7 @@
import { opencodeClient } from '@/lib/opencode/client';
import { substituteCommandVariables } from '@/lib/openchamberConfig';
import type { WorktreeMetadata } from '@/types/worktree';
import { deleteRemoteBranch } from '@/lib/gitApi';
import { deleteRemoteBranch, getGitStatus } from '@/lib/gitApi';
export type ProjectRef = { id: string; path: string };
@@ -143,6 +143,21 @@ export async function listProjectWorktrees(project: ProjectRef): Promise<Worktre
} catch {
// ignore
}
// Enrich worktrees with branch information from git status
await Promise.all(
results.map(async (worktree) => {
try {
const status = await getGitStatus(worktree.path);
if (status?.current) {
worktree.branch = status.current;
}
} catch {
// ignore - branch will remain empty
}
})
);
return results.sort((a, b) => {
const aLabel = (a.label || a.branch || a.path).toLowerCase();
const bLabel = (b.label || b.branch || b.path).toLowerCase();