fix: highlight project row in sidebar for new conversations, clear for standalone drafts (#116)

* fix: highlight project row in sidebar when starting new chat in a project

When starting a new conversation with a project selected, the sidebar
now highlights the project row instead of leaving the previous session
highlighted (or showing no highlight at all).

The root cause: draft sessions are excluded from the sidebar's session
list, so the activeSessionRefCallback never fires for them. The
highlight useEffect would early-return when activeSessionId was set,
leaving a stale highlight.

The fix computes an activeProjectId when the active session is a draft
belonging to a project, and uses an activeProjectRefCallback to position
the sliding highlight on the parent project row.

* Clear sidebar highlight for standalone draft sessions

When starting a new conversation without a project, the sidebar now
shows no highlight instead of keeping the previous selection.

The highlight logic now distinguishes three draft cases:
- Draft in project → highlight the project row (previous commit)
- Standalone draft → clear the highlight entirely (this commit)
- Non-draft session → ref callback positions the highlight as before
This commit is contained in:
Matt Toohey
2026-04-08 14:26:21 +10:00
committed by GitHub
parent 2a8fa0d3bc
commit 7ff4d2a6a4
3 changed files with 54 additions and 20 deletions
+5 -4
View File
@@ -11,9 +11,9 @@ const EXCEPTIONS = {
"Temporary allowance while project creation, home/chat handoff, session-history routing, and drag-drop wiring remain in the shell.",
},
"src/features/sidebar/ui/SidebarProjectsSection.tsx": {
limit: 545,
limit: 560,
justification:
"Added drag-and-drop handlers for moving chats between projects.",
"Drag-and-drop handlers plus activeProjectId highlight for draft-in-project sessions.",
},
"src/features/chat/ui/ChatView.tsx": {
limit: 505,
@@ -21,8 +21,9 @@ const EXCEPTIONS = {
"Added useSessionAutoTitle hook call for smart session naming.",
},
"src/features/sidebar/ui/Sidebar.tsx": {
limit: 575,
justification: "Added search-as-you-type filtering for sidebar sessions.",
limit: 580,
justification:
"Search-as-you-type filtering plus draft-aware sidebar highlight logic.",
},
"src-tauri/src/services/acp/manager.rs": {
limit: 520,
+30 -14
View File
@@ -260,12 +260,8 @@ export function Sidebar({
return () => window.removeEventListener("keydown", handler);
}, []);
const toggleProject = (projectId: string) => {
setExpandedProjects((prev) => ({
...prev,
[projectId]: !prev[projectId],
}));
};
const toggleProject = (projectId: string) =>
setExpandedProjects((prev) => ({ ...prev, [projectId]: !prev[projectId] }));
const navRef = useRef<HTMLElement>(null);
const homeRef = useRef<HTMLButtonElement>(null);
@@ -279,12 +275,20 @@ export function Sidebar({
updateActiveRect,
} = useSidebarHighlight(navRef);
// Update active rect when activeView/activeSessionId changes
// Drafts aren't in the sidebar — highlight project row or clear highlight.
const activeDraft = activeSessionId
? sessions.find((s) => s.id === activeSessionId && s.draft)
: undefined;
const activeProjectId = activeDraft?.projectId ?? null;
// Position the sliding highlight based on what's active.
// Non-draft sessions and draft-in-project are handled by ref callbacks.
useEffect(() => {
if (activeSessionId) {
if (activeDraft) {
if (!activeProjectId) updateActiveRect(null);
return;
}
if (activeSessionId) return;
if (activeView === "home") {
updateActiveRect(homeRef.current);
} else if (activeView && navItemRefs.current[activeView]) {
@@ -292,17 +296,27 @@ export function Sidebar({
} else {
updateActiveRect(null);
}
}, [activeSessionId, activeView, updateActiveRect]);
}, [
activeSessionId,
activeDraft,
activeProjectId,
activeView,
updateActiveRect,
]);
// Callback for SidebarProjectsSection to register active session refs
// Ref callbacks to position the highlight on active session or project row.
const activeSessionRefCallback = useCallback(
(el: HTMLElement | null) => {
if (activeSessionId && el) {
updateActiveRect(el);
}
if (activeSessionId && el) updateActiveRect(el);
},
[activeSessionId, updateActiveRect],
);
const activeProjectRefCallback = useCallback(
(el: HTMLElement | null) => {
if (activeProjectId && el) updateActiveRect(el);
},
[activeProjectId, updateActiveRect],
);
return (
<div
@@ -538,6 +552,7 @@ export function Sidebar({
labelTransition={labelTransition}
labelVisible={labelVisible}
activeSessionId={activeSessionId}
activeProjectId={activeProjectId}
onNavigate={onNavigate}
onSelectSession={onSelectSession}
onNewChatInProject={onNewChatInProject}
@@ -550,6 +565,7 @@ export function Sidebar({
onMoveToProject={onMoveToProject}
onItemMouseEnter={onItemMouseEnter}
activeSessionRefCallback={activeSessionRefCallback}
activeProjectRefCallback={activeProjectRefCallback}
/>
</>
)}
@@ -46,6 +46,7 @@ interface SidebarProjectsSectionProps {
labelTransition: string;
labelVisible: boolean;
activeSessionId?: string | null;
activeProjectId?: string | null;
onNavigate?: (view: AppView) => void;
onSelectSession?: (sessionId: string) => void;
onNewChatInProject?: (projectId: string) => void;
@@ -58,6 +59,7 @@ interface SidebarProjectsSectionProps {
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
onItemMouseEnter?: (e: React.MouseEvent<HTMLElement>) => void;
activeSessionRefCallback?: (el: HTMLElement | null) => void;
activeProjectRefCallback?: (el: HTMLElement | null) => void;
}
function ItemMenu({
label,
@@ -112,6 +114,7 @@ function ProjectSection({
isExpanded,
toggleProject,
activeSessionId,
activeProjectId,
onSelectSession,
onNewChatInProject,
onNavigate,
@@ -122,12 +125,14 @@ function ProjectSection({
onMoveToProject,
onItemMouseEnter,
activeSessionRefCallback,
activeProjectRefCallback,
}: {
project: ProjectInfo;
projectChats: TabInfo[];
isExpanded: boolean;
toggleProject: (projectId: string) => void;
activeSessionId?: string | null;
activeProjectId?: string | null;
onSelectSession?: (sessionId: string) => void;
onNewChatInProject?: (projectId: string) => void;
onNavigate?: (view: AppView) => void;
@@ -138,6 +143,7 @@ function ProjectSection({
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
onItemMouseEnter?: (e: React.MouseEvent<HTMLElement>) => void;
activeSessionRefCallback?: (el: HTMLElement | null) => void;
activeProjectRefCallback?: (el: HTMLElement | null) => void;
}) {
const [showAll, setShowAll] = useState(false);
const [dragOver, setDragOver] = useState(false);
@@ -180,7 +186,12 @@ function ProjectSection({
onDrop={handleDrop}
>
{/* Project row */}
<div className="relative flex items-center group rounded-md transition-colors duration-200">
<div
ref={
activeProjectId === project.id ? activeProjectRefCallback : undefined
}
className="relative flex items-center group rounded-md transition-colors duration-200"
>
<Button
type="button"
variant="ghost"
@@ -189,7 +200,9 @@ function ProjectSection({
onMouseEnter={onItemMouseEnter}
className={cn(
"flex-1 min-w-0 justify-start gap-2 rounded-md px-3 py-2 text-[13px] font-light",
PROJECT_ROW_TEXT_CLASS,
activeProjectId === project.id
? "text-foreground hover:bg-transparent hover:text-foreground group-hover:text-foreground"
: PROJECT_ROW_TEXT_CLASS,
)}
>
<span className="relative flex h-3 w-3 flex-shrink-0 items-center justify-center">
@@ -302,6 +315,7 @@ export function SidebarProjectsSection({
labelTransition,
labelVisible,
activeSessionId,
activeProjectId,
onNavigate,
onSelectSession,
onNewChatInProject,
@@ -314,6 +328,7 @@ export function SidebarProjectsSection({
onMoveToProject,
onItemMouseEnter,
activeSessionRefCallback,
activeProjectRefCallback,
}: SidebarProjectsSectionProps) {
const [recentsDragOver, setRecentsDragOver] = useState(false);
@@ -420,6 +435,7 @@ export function SidebarProjectsSection({
isExpanded={expandedProjects[project.id] ?? false}
toggleProject={toggleProject}
activeSessionId={activeSessionId}
activeProjectId={activeProjectId}
onSelectSession={onSelectSession}
onNewChatInProject={onNewChatInProject}
onNavigate={onNavigate}
@@ -430,6 +446,7 @@ export function SidebarProjectsSection({
onMoveToProject={onMoveToProject}
onItemMouseEnter={onItemMouseEnter}
activeSessionRefCallback={activeSessionRefCallback}
activeProjectRefCallback={activeProjectRefCallback}
/>
))}
</div>