feat: enhance SessionSidebar with worktree metadata and UI updates
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
|||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
RiErrorWarningLine,
|
RiErrorWarningLine,
|
||||||
RiFileCopyLine,
|
RiFileCopyLine,
|
||||||
|
RiGitBranchLine,
|
||||||
RiGitRepositoryLine,
|
RiGitRepositoryLine,
|
||||||
RiLinkUnlinkM,
|
RiLinkUnlinkM,
|
||||||
RiMore2Line,
|
RiMore2Line,
|
||||||
@@ -55,7 +56,6 @@ import { opencodeClient } from '@/lib/opencode/client';
|
|||||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||||
|
|
||||||
const GROUP_COLLAPSE_STORAGE_KEY = 'oc.sessions.groupCollapse';
|
|
||||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
|
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
|
||||||
const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents';
|
const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents';
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ const formatProjectLabel = (label: string): string => {
|
|||||||
type SessionNode = {
|
type SessionNode = {
|
||||||
session: Session;
|
session: Session;
|
||||||
children: SessionNode[];
|
children: SessionNode[];
|
||||||
|
worktree: WorktreeMetadata | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SessionGroup = {
|
type SessionGroup = {
|
||||||
@@ -337,17 +338,13 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
);
|
);
|
||||||
const checkingDirectories = React.useRef<Set<string>>(new Set());
|
const checkingDirectories = React.useRef<Set<string>>(new Set());
|
||||||
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
||||||
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
|
|
||||||
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
|
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
||||||
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
||||||
const [hoveredGroupId, setHoveredGroupId] = React.useState<string | null>(null);
|
|
||||||
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
||||||
const [activeDragId, setActiveDragId] = React.useState<string | null>(null);
|
const [activeDragId, setActiveDragId] = React.useState<string | null>(null);
|
||||||
const [stuckHeaders, setStuckHeaders] = React.useState<Set<string>>(new Set());
|
|
||||||
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
||||||
const headerSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
|
|
||||||
const projectHeaderSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
|
const projectHeaderSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||||
const ignoreIntersectionUntil = React.useRef<number>(0);
|
const ignoreIntersectionUntil = React.useRef<number>(0);
|
||||||
|
|
||||||
@@ -380,7 +377,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
|
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
|
||||||
const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
|
const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
|
||||||
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
||||||
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
|
||||||
|
|
||||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -391,13 +387,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const storedGroups = safeStorage.getItem(GROUP_COLLAPSE_STORAGE_KEY);
|
|
||||||
if (storedGroups) {
|
|
||||||
const parsed = JSON.parse(storedGroups);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
setCollapsedGroups(new Set(parsed.filter((item) => typeof item === 'string')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const storedParents = safeStorage.getItem(SESSION_EXPANDED_STORAGE_KEY);
|
const storedParents = safeStorage.getItem(SESSION_EXPANDED_STORAGE_KEY);
|
||||||
if (storedParents) {
|
if (storedParents) {
|
||||||
const parsed = JSON.parse(storedParents);
|
const parsed = JSON.parse(storedParents);
|
||||||
@@ -733,20 +722,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
[collectDescendants, deleteSession, deleteSessions, worktreeMetadata],
|
[collectDescendants, deleteSession, deleteSessions, worktreeMetadata],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateSessionInGroup = React.useCallback(
|
|
||||||
(directory: string | null, projectId?: string | null) => {
|
|
||||||
if (projectId && projectId !== activeProjectId) {
|
|
||||||
setActiveProject(projectId);
|
|
||||||
}
|
|
||||||
setActiveMainTab('chat');
|
|
||||||
if (mobileVariant) {
|
|
||||||
setSessionSwitcherOpen(false);
|
|
||||||
}
|
|
||||||
openNewSessionDraft({ directoryOverride: directory ?? null });
|
|
||||||
},
|
|
||||||
[activeProjectId, openNewSessionDraft, setActiveMainTab, setActiveProject, setSessionSwitcherOpen, mobileVariant],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenWorktreeManager = React.useCallback(
|
const handleOpenWorktreeManager = React.useCallback(
|
||||||
(projectId?: string | null) => {
|
(projectId?: string | null) => {
|
||||||
if (projectId && projectId !== activeProjectId) {
|
if (projectId && projectId !== activeProjectId) {
|
||||||
@@ -805,15 +780,15 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
children: children.map((child) => buildNode(child)),
|
children: children.map((child) => buildNode(child)),
|
||||||
|
worktree: worktreeMetadata.get(session.id) ?? null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[childrenMap],
|
[childrenMap, worktreeMetadata],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const buildGroupedSessions = React.useCallback(
|
const buildGroupedSessions = React.useCallback(
|
||||||
(projectSessions: Session[], projectRoot: string | null, availableWorktrees: WorktreeMetadata[]) => {
|
(projectSessions: Session[], projectRoot: string | null, availableWorktrees: WorktreeMetadata[]) => {
|
||||||
const groups = new Map<string, SessionGroup>();
|
|
||||||
const normalizedProjectRoot = normalizePath(projectRoot ?? null);
|
const normalizedProjectRoot = normalizePath(projectRoot ?? null);
|
||||||
const sortedProjectSessions = [...projectSessions].sort((a, b) => (b.time?.created || 0) - (a.time?.created || 0));
|
const sortedProjectSessions = [...projectSessions].sort((a, b) => (b.time?.created || 0) - (a.time?.created || 0));
|
||||||
|
|
||||||
@@ -830,14 +805,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
});
|
});
|
||||||
childrenMap.forEach((list) => list.sort((a, b) => (b.time?.created || 0) - (a.time?.created || 0)));
|
childrenMap.forEach((list) => list.sort((a, b) => (b.time?.created || 0) - (a.time?.created || 0)));
|
||||||
|
|
||||||
const buildProjectNode = (session: Session): SessionNode => {
|
// Build worktree lookup map
|
||||||
const children = childrenMap.get(session.id) ?? [];
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
children: children.map((child) => buildProjectNode(child)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const worktreeByPath = new Map<string, WorktreeMetadata>();
|
const worktreeByPath = new Map<string, WorktreeMetadata>();
|
||||||
availableWorktrees.forEach((meta) => {
|
availableWorktrees.forEach((meta) => {
|
||||||
if (meta.path) {
|
if (meta.path) {
|
||||||
@@ -846,42 +814,31 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ensureGroup = (session: Session) => {
|
// Helper to get worktree metadata for a session
|
||||||
|
const getSessionWorktree = (session: Session): WorktreeMetadata | null => {
|
||||||
const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
|
const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
|
||||||
|
|
||||||
const sessionWorktreeMeta = worktreeMetadata.get(session.id) ?? null;
|
const sessionWorktreeMeta = worktreeMetadata.get(session.id) ?? null;
|
||||||
const worktree =
|
if (sessionWorktreeMeta) return sessionWorktreeMeta;
|
||||||
sessionWorktreeMeta ??
|
if (sessionDirectory) {
|
||||||
(sessionDirectory ? worktreeByPath.get(sessionDirectory) ?? null : null);
|
const worktree = worktreeByPath.get(sessionDirectory) ?? null;
|
||||||
const isMain =
|
// Only count as worktree if it's not the main project root
|
||||||
!worktree &&
|
if (worktree && sessionDirectory !== normalizedProjectRoot) {
|
||||||
((sessionDirectory && normalizedProjectRoot
|
return worktree;
|
||||||
? sessionDirectory === normalizedProjectRoot
|
}
|
||||||
: !sessionDirectory && Boolean(normalizedProjectRoot)));
|
|
||||||
const key = isMain ? 'main' : worktree?.path ?? sessionDirectory ?? session.id;
|
|
||||||
const directory = worktree?.path ?? sessionDirectory ?? normalizedProjectRoot ?? null;
|
|
||||||
if (!groups.has(key)) {
|
|
||||||
const label = isMain
|
|
||||||
? 'Main workspace'
|
|
||||||
: worktree?.label || worktree?.branch || formatDirectoryName(directory || '', homeDirectory) || 'Worktree';
|
|
||||||
const description = worktree?.relativePath
|
|
||||||
? formatPathForDisplay(worktree.relativePath, homeDirectory)
|
|
||||||
: directory
|
|
||||||
? formatPathForDisplay(directory, homeDirectory)
|
|
||||||
: null;
|
|
||||||
groups.set(key, {
|
|
||||||
id: key,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
isMain,
|
|
||||||
worktree,
|
|
||||||
directory,
|
|
||||||
sessions: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return groups.get(key)!;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildProjectNode = (session: Session): SessionNode => {
|
||||||
|
const children = childrenMap.get(session.id) ?? [];
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
children: children.map((child) => buildProjectNode(child)),
|
||||||
|
worktree: getSessionWorktree(session),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find root sessions (no parent or parent not in current project)
|
||||||
const roots = sortedProjectSessions.filter((session) => {
|
const roots = sortedProjectSessions.filter((session) => {
|
||||||
const parentID = (session as Session & { parentID?: string | null }).parentID;
|
const parentID = (session as Session & { parentID?: string | null }).parentID;
|
||||||
if (!parentID) {
|
if (!parentID) {
|
||||||
@@ -890,70 +847,23 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
return !sessionMap.has(parentID);
|
return !sessionMap.has(parentID);
|
||||||
});
|
});
|
||||||
|
|
||||||
roots.forEach((session) => {
|
// Build all session nodes into a single flat group sorted by date
|
||||||
const group = ensureGroup(session);
|
const allSessions: SessionNode[] = roots.map((session) => buildProjectNode(session));
|
||||||
const node = buildProjectNode(session);
|
|
||||||
group.sessions.push(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!groups.has('main')) {
|
// Return single group with all sessions
|
||||||
groups.set('main', {
|
return [{
|
||||||
id: 'main',
|
id: 'all',
|
||||||
label: 'Main workspace',
|
label: 'All sessions',
|
||||||
description: normalizedProjectRoot ? formatPathForDisplay(normalizedProjectRoot, homeDirectory) : null,
|
description: normalizedProjectRoot ? formatPathForDisplay(normalizedProjectRoot, homeDirectory) : null,
|
||||||
isMain: true,
|
isMain: true,
|
||||||
worktree: null,
|
worktree: null,
|
||||||
directory: normalizedProjectRoot,
|
directory: normalizedProjectRoot,
|
||||||
sessions: [],
|
sessions: allSessions,
|
||||||
});
|
}];
|
||||||
}
|
|
||||||
|
|
||||||
worktreeByPath.forEach((meta, path) => {
|
|
||||||
const key = meta.path;
|
|
||||||
if (!groups.has(key)) {
|
|
||||||
groups.set(key, {
|
|
||||||
id: key,
|
|
||||||
label: meta.label || meta.branch || formatDirectoryName(path, homeDirectory) || 'Worktree',
|
|
||||||
description: meta.relativePath
|
|
||||||
? formatPathForDisplay(meta.relativePath, homeDirectory)
|
|
||||||
: formatPathForDisplay(path, homeDirectory),
|
|
||||||
isMain: false,
|
|
||||||
worktree: meta,
|
|
||||||
directory: path,
|
|
||||||
sessions: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
groups.forEach((group) => {
|
|
||||||
group.sessions.sort((a, b) => (b.session.time?.created || 0) - (a.session.time?.created || 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(groups.values()).sort((a, b) => {
|
|
||||||
if (a.isMain !== b.isMain) {
|
|
||||||
return a.isMain ? -1 : 1;
|
|
||||||
}
|
|
||||||
return (a.label || '').localeCompare(b.label || '');
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[homeDirectory, worktreeMetadata]
|
[homeDirectory, worktreeMetadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleGroup = React.useCallback((groupId: string) => {
|
|
||||||
setCollapsedGroups((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(groupId)) {
|
|
||||||
next.delete(groupId);
|
|
||||||
} else {
|
|
||||||
next.add(groupId);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
safeStorage.setItem(GROUP_COLLAPSE_STORAGE_KEY, JSON.stringify(Array.from(next)));
|
|
||||||
} catch { /* ignored */ }
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, [safeStorage]);
|
|
||||||
|
|
||||||
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
||||||
setExpandedSessionGroups((prev) => {
|
setExpandedSessionGroups((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1038,47 +948,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
});
|
});
|
||||||
}, [normalizedProjects, getSessionsForProject, buildGroupedSessions, availableWorktreesByProject]);
|
}, [normalizedProjects, getSessionsForProject, buildGroupedSessions, availableWorktreesByProject]);
|
||||||
|
|
||||||
// Track when sticky headers become "stuck" using sentinel elements
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isDesktopRuntime) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
// Ignore intersection events shortly after collapse/expand
|
|
||||||
if (Date.now() < ignoreIntersectionUntil.current) return;
|
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const groupId = (entry.target as HTMLElement).dataset.groupId;
|
|
||||||
if (!groupId) return;
|
|
||||||
|
|
||||||
setStuckHeaders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
// When sentinel is NOT intersecting, header is stuck
|
|
||||||
if (!entry.isIntersecting) {
|
|
||||||
next.add(groupId);
|
|
||||||
} else {
|
|
||||||
next.delete(groupId);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0, rootMargin: '-96px 0px 0px 0px' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Small delay to let DOM settle after collapse/expand
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
headerSentinelRefs.current.forEach((el) => {
|
|
||||||
if (el) observer.observe(el);
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [isDesktopRuntime, projectSections, collapsedProjects]);
|
|
||||||
|
|
||||||
// Track when project sticky headers become "stuck"
|
// Track when project sticky headers become "stuck"
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isDesktopRuntime) return;
|
if (!isDesktopRuntime) return;
|
||||||
@@ -1287,6 +1156,18 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
{session.share ? (
|
{session.share ? (
|
||||||
<RiShare2Line className="h-3 w-3 text-[color:var(--status-info)] flex-shrink-0" />
|
<RiShare2Line className="h-3 w-3 text-[color:var(--status-info)] flex-shrink-0" />
|
||||||
) : null}
|
) : null}
|
||||||
|
{node.worktree ? (
|
||||||
|
<Tooltip delayDuration={700}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
<RiGitBranchLine className="h-3 w-3 text-[color:var(--status-info)]" />
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="text-xs">
|
||||||
|
{node.worktree.branch || node.worktree.label || 'Worktree'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
{hasSummary && ((additions ?? 0) !== 0 || (deletions ?? 0) !== 0) ? (
|
{hasSummary && ((additions ?? 0) !== 0 || (deletions ?? 0) !== 0) ? (
|
||||||
<span className="flex-shrink-0 text-[0.7rem] leading-none">
|
<span className="flex-shrink-0 text-[0.7rem] leading-none">
|
||||||
<span className="text-[color:var(--status-success)]">+{Math.max(0, additions ?? 0)}</span>
|
<span className="text-[color:var(--status-success)]">+{Math.max(0, additions ?? 0)}</span>
|
||||||
@@ -1627,77 +1508,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
sentinelRef={(el) => { projectHeaderSentinelRefs.current.set(projectKey, el); }}
|
sentinelRef={(el) => { projectHeaderSentinelRefs.current.set(projectKey, el); }}
|
||||||
>
|
>
|
||||||
{!isCollapsed ? (
|
{!isCollapsed ? (
|
||||||
<div className="space-y-2 pl-1">
|
<div className="space-y-[0.6rem] py-1 pl-1">
|
||||||
{section.groups.map((group) => {
|
{section.groups[0] ? renderGroupSessions(section.groups[0], `${projectKey}:all`, projectKey) : (
|
||||||
const groupKey = `${projectKey}:${group.id}`;
|
<div className="py-1 text-left typography-micro text-muted-foreground">
|
||||||
return (
|
No sessions yet.
|
||||||
<div key={groupKey} className="relative">
|
</div>
|
||||||
{isDesktopRuntime && (
|
)}
|
||||||
<div
|
|
||||||
ref={(el) => { headerSentinelRefs.current.set(groupKey, el); }}
|
|
||||||
data-group-id={groupKey}
|
|
||||||
className="absolute top-0 h-px w-full pointer-events-none"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleGroup(groupKey)}
|
|
||||||
className={cn(
|
|
||||||
'sticky top-[38px] z-[9] pt-1.5 pb-1 w-full text-left cursor-pointer group/header border-b',
|
|
||||||
!isDesktopRuntime && 'bg-sidebar',
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDesktopRuntime
|
|
||||||
? stuckHeaders.has(groupKey) ? 'var(--sidebar-stuck-bg)' : 'transparent'
|
|
||||||
: undefined,
|
|
||||||
borderColor: hoveredGroupId === groupKey
|
|
||||||
? 'var(--color-border)'
|
|
||||||
: collapsedGroups.has(groupKey)
|
|
||||||
? 'color-mix(in srgb, var(--color-border) 35%, transparent)'
|
|
||||||
: 'var(--color-border)'
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setHoveredGroupId(groupKey)}
|
|
||||||
onMouseLeave={() => setHoveredGroupId(null)}
|
|
||||||
aria-label={collapsedGroups.has(groupKey) ? 'Expand group' : 'Collapse group'}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2 px-1">
|
|
||||||
<span className="typography-micro font-medium text-muted-foreground truncate group-hover/header:text-foreground">
|
|
||||||
{group.label}
|
|
||||||
</span>
|
|
||||||
{!hideDirectoryControls && (
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
|
|
||||||
)}
|
|
||||||
aria-label="Create session in this group"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCreateSessionInGroup(group.directory, projectKey);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCreateSessionInGroup(group.directory, projectKey);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAddLine className="h-4.5 w-4.5" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{!collapsedGroups.has(groupKey) ? (
|
|
||||||
<div className="space-y-[0.6rem] py-1">
|
|
||||||
{renderGroupSessions(group, groupKey, projectKey)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</SortableProjectItem>
|
</SortableProjectItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user