feat(sidebar): project notes/todos live in right sidebar context tab

Replace the header sticky-note popover/mobile overlay with a dedicated Context
tab in the right sidebar. Context is cycled by the right-sidebar shortcut
alongside git and files, and persisted across reloads.
This commit is contained in:
Bohdan Triapitsyn
2026-04-18 13:46:57 +03:00
parent a9c51f115e
commit d203ba2ae0
5 changed files with 70 additions and 129 deletions
@@ -4,7 +4,6 @@ import { RiLayoutLeftLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, startDesktopWindowDrag } from '@/lib/desktop';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { sessionEvents } from '@/lib/sessionEvents';
import { formatDirectoryName, cn } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
@@ -16,11 +15,9 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
import { useUIStore } from '@/stores/useUIStore';
import { getSafeStorage } from '@/stores/utils/safeStorage';
import { useGitStore, useGitAllBranches, useGitRepoStatusMap } from '@/stores/useGitStore';
import { useDeviceInfo } from '@/lib/device';
import { isVSCodeRuntime } from '@/lib/desktop';
import { NewWorktreeDialog } from './NewWorktreeDialog';
import { ScheduledTasksDialog } from './ScheduledTasksDialog';
import { ProjectNotesTodoPanel } from './ProjectNotesTodoPanel';
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useArchivedAutoFolders } from './sidebar/hooks/useArchivedAutoFolders';
@@ -142,7 +139,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false);
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false);
const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState<string | null>(null);
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
const [renameFolderDraft, setRenameFolderDraft] = React.useState('');
@@ -230,7 +226,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
const setAboutDialogOpen = useUIStore((state) => state.setAboutDialogOpen);
const deviceInfo = useDeviceInfo();
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const setScheduledTasksDialogOpen = useUIStore((state) => state.setScheduledTasksDialogOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
@@ -893,44 +888,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
</div>
);
const activeProjectForHeader = React.useMemo(
() => normalizedProjects.find((project) => project.id === activeProjectId) ?? normalizedProjects[0] ?? null,
[normalizedProjects, activeProjectId],
);
const activeProjectRefForHeader = React.useMemo(
() => (activeProjectForHeader
? {
id: activeProjectForHeader.id,
path: activeProjectForHeader.normalizedPath,
}
: null),
[activeProjectForHeader],
);
const activeProjectLabelForHeader = React.useMemo(
() => (activeProjectForHeader
? activeProjectForHeader.label?.trim()
|| formatDirectoryName(activeProjectForHeader.normalizedPath, homeDirectory)
|| activeProjectForHeader.normalizedPath
: null),
[activeProjectForHeader, homeDirectory],
);
const activeProjectIsRepo = React.useMemo(
() => (activeProjectForHeader ? Boolean(projectRepoStatus.get(activeProjectForHeader.id)) : false),
[activeProjectForHeader, projectRepoStatus],
);
// Only flip to false once the new project's status is actually resolved (present in map)
const stableActiveProjectIsRepo = activeProjectForHeader && projectRepoStatus.has(activeProjectForHeader.id)
? activeProjectIsRepo
: lastRepoStatusRef.current;
const reserveHeaderActionsSpace = true;
const useMobileNotesPanel = mobileVariant || deviceInfo.isMobile;
React.useEffect(() => {
if (!activeProjectForHeader) {
setProjectNotesPanelOpen(false);
}
}, [activeProjectForHeader]);
const { currentSessionDirectory } = useProjectSessionSelection({
projectSections,
@@ -1479,14 +1437,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
hideDirectoryControls={hideDirectoryControls}
handleOpenDirectoryDialog={handleOpenDirectoryDialog}
handleNewSession={handleSidebarNewSession}
useMobileNotesPanel={useMobileNotesPanel}
projectNotesPanelOpen={projectNotesPanelOpen}
setProjectNotesPanelOpen={setProjectNotesPanelOpen}
activeProjectRefForHeader={activeProjectRefForHeader}
activeProjectLabelForHeader={activeProjectLabelForHeader}
canOpenMultiRun={projects.length > 0}
openMultiRunLauncher={handleOpenMultiRunFromHeader}
stableActiveProjectIsRepo={stableActiveProjectIsRepo}
headerActionIconClass={headerActionIconClass}
reserveHeaderActionsSpace={reserveHeaderActionsSpace}
headerActionButtonClass={headerActionButtonClass}
@@ -1594,22 +1546,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
<ScheduledTasksDialog />
{useMobileNotesPanel ? (
<MobileOverlayPanel
open={projectNotesPanelOpen}
onClose={() => setProjectNotesPanelOpen(false)}
title={activeProjectLabelForHeader ? `Project notes - ${activeProjectLabelForHeader}` : 'Project notes'}
>
<ProjectNotesTodoPanel
projectRef={activeProjectRefForHeader}
projectLabel={activeProjectLabelForHeader}
canCreateWorktree={stableActiveProjectIsRepo}
onActionComplete={() => setProjectNotesPanelOpen(false)}
className="p-0"
/>
</MobileOverlayPanel>
) : null}
<SessionDeleteConfirmDialog
value={deleteSessionConfirm}
setValue={setDeleteSessionConfirm}
@@ -16,26 +16,17 @@ import {
RiCloseLine,
RiContractUpDownLine,
RiExpandUpDownLine,
RiStickyNoteLine,
RiCalendarScheduleLine,
} from '@remixicon/react';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import type { ProjectRef } from '@/lib/openchamberConfig';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { ProjectNotesTodoPanel } from '../ProjectNotesTodoPanel';
type Props = {
hideDirectoryControls: boolean;
handleOpenDirectoryDialog: () => void;
handleNewSession: () => void;
useMobileNotesPanel: boolean;
projectNotesPanelOpen: boolean;
setProjectNotesPanelOpen: (open: boolean) => void;
activeProjectRefForHeader: ProjectRef | null;
activeProjectLabelForHeader: string | null;
canOpenMultiRun: boolean;
openMultiRunLauncher: () => void;
stableActiveProjectIsRepo: boolean;
headerActionIconClass: string;
reserveHeaderActionsSpace: boolean;
headerActionButtonClass: string;
@@ -56,14 +47,8 @@ export function SidebarHeader(props: Props): React.ReactNode {
hideDirectoryControls,
handleOpenDirectoryDialog,
handleNewSession,
useMobileNotesPanel,
projectNotesPanelOpen,
setProjectNotesPanelOpen,
activeProjectRefForHeader,
activeProjectLabelForHeader,
canOpenMultiRun,
openMultiRunLauncher,
stableActiveProjectIsRepo,
headerActionIconClass,
reserveHeaderActionsSpace,
headerActionButtonClass,
@@ -150,49 +135,6 @@ export function SidebarHeader(props: Props): React.ReactNode {
<TooltipContent side="bottom" sideOffset={4}><p>Scheduled tasks</p></TooltipContent>
</Tooltip>
{useMobileNotesPanel ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setProjectNotesPanelOpen(true)}
className={headerActionButtonClass}
aria-label="Project notes"
disabled={!activeProjectRefForHeader}
>
<RiStickyNoteLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Project notes</p></TooltipContent>
</Tooltip>
) : (
<DropdownMenu open={projectNotesPanelOpen} onOpenChange={setProjectNotesPanelOpen} modal={false}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
className={headerActionButtonClass}
aria-label="Project notes"
disabled={!activeProjectRefForHeader}
>
<RiStickyNoteLine className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Project notes</p></TooltipContent>
</Tooltip>
<DropdownMenuContent align="start" className="w-[420px] max-w-[min(92vw,420px)] p-0">
<ProjectNotesTodoPanel
projectRef={activeProjectRefForHeader}
projectLabel={activeProjectLabelForHeader}
canCreateWorktree={stableActiveProjectIsRepo}
onActionComplete={() => setProjectNotesPanelOpen(false)}
/>
</DropdownMenuContent>
</DropdownMenu>
)}
<Tooltip>
<TooltipTrigger asChild>
<button