diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 13e22bbf..53ab8f53 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ }>; }, [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 = ({ return () => { cancelled = true; }; - }, [normalizedProjects]); + }, [normalizedProjects, projectGitBranchesKey]); const getSessionsForProject = React.useCallback( (project: { normalizedPath: string }) => { @@ -2263,7 +2293,7 @@ export const SessionSidebar: React.FC = ({ return (
{ if (!group.isMain) { void ensureWorktreePrLoaded(groupKey, group.directory, group.label, group.worktree); @@ -2304,11 +2334,11 @@ export const SessionSidebar: React.FC = ({ }} aria-label={isCollapsed ? `Expand ${group.label}` : `Collapse ${group.label}`} > -
+
{isCollapsed ? ( - + ) : ( - + )} {!group.isMain || isGitProject ? ( !group.isMain && groupPr?.url ? ( @@ -2322,7 +2352,7 @@ export const SessionSidebar: React.FC = ({ 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 = ({ ) : ( - + ) ) : null} -

- {group.label} -

+
+

+ {group.label} +

+ {!group.isMain && isBranchDifferentFromLabel(group.branch, group.label) ? ( + + {group.branch} + + ) : null} +
{group.directory ? (
diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 03310083..a9655376 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -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), diff --git a/packages/ui/src/lib/worktrees/worktreeManager.ts b/packages/ui/src/lib/worktrees/worktreeManager.ts index eb284581..80b217be 100644 --- a/packages/ui/src/lib/worktrees/worktreeManager.ts +++ b/packages/ui/src/lib/worktrees/worktreeManager.ts @@ -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 { + 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();