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
@@ -1,15 +1,19 @@
import React from 'react';
import { RiFolder3Line, RiGitBranchLine } from '@remixicon/react';
import { RiBookletLine, RiFolder3Line, RiGitBranchLine } from '@remixicon/react';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { ProjectNotesTodoPanel } from '@/components/session/ProjectNotesTodoPanel';
import { GitView } from '@/components/views';
import { useUIStore } from '@/stores/useUIStore';
import { useGitStore } from '@/stores/useGitStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useUIStore } from '@/stores/useUIStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { formatDirectoryName } from '@/lib/utils';
import { SidebarFilesTree } from './SidebarFilesTree';
type RightTab = 'git' | 'files';
type RightTab = 'git' | 'files' | 'context';
/**
* Keeps git status fresh while the right sidebar is open.
@@ -35,6 +39,56 @@ function useRightSidebarGitSync(directory: string | undefined, isSidebarOpen: bo
}, [directory, git, isSidebarOpen, ensureStatus]);
}
const ContextSidebarPanel: React.FC = () => {
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const projects = useProjectsStore((state) => state.projects);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
const gitDirectories = useGitStore((state) => state.directories);
const activeProject = React.useMemo(() => {
if (activeProjectId) {
return projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
}
return projects[0] ?? null;
}, [activeProjectId, projects]);
const projectRef = React.useMemo(() => {
if (!activeProject) {
return null;
}
return {
id: activeProject.id,
path: activeProject.path,
};
}, [activeProject]);
const projectLabel = React.useMemo(() => {
if (!activeProject) {
return null;
}
return activeProject.label?.trim()
|| formatDirectoryName(activeProject.path, homeDirectory)
|| activeProject.path;
}, [activeProject, homeDirectory]);
const canCreateWorktree = React.useMemo(() => {
if (!activeProject) {
return false;
}
return gitDirectories.get(activeProject.path)?.isGitRepo === true;
}, [activeProject, gitDirectories]);
return (
<div className="h-full min-h-0 overflow-auto bg-sidebar">
<ProjectNotesTodoPanel
projectRef={projectRef}
projectLabel={projectLabel}
canCreateWorktree={canCreateWorktree}
/>
</div>
);
};
export const RightSidebarTabs: React.FC = () => {
const rightSidebarTab = useUIStore((state) => state.rightSidebarTab);
const setRightSidebarTab = useUIStore((state) => state.setRightSidebarTab);
@@ -54,6 +108,11 @@ export const RightSidebarTabs: React.FC = () => {
label: 'Files',
icon: <RiFolder3Line className="h-3.5 w-3.5" />,
},
{
id: 'context',
label: 'Context',
icon: <RiBookletLine className="h-3.5 w-3.5" />,
},
], []);
return (
@@ -72,7 +131,8 @@ export const RightSidebarTabs: React.FC = () => {
<div className="min-h-0 flex-1 overflow-hidden">
{rightSidebarTab === 'git' && <GitView />}
{rightSidebarTab === 'files' && <SidebarFilesTree />}
{rightSidebarTab === 'context' && <ContextSidebarPanel />}
</div>
</div>
);
};
};
@@ -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
@@ -182,7 +182,7 @@ export const useKeyboardShortcuts = () => {
return;
}
const tabs = ['git', 'files'] as const;
const tabs = ['git', 'files', 'context'] as const;
const currentIndex = tabs.indexOf(rightSidebarTab);
const nextTab = tabs[(currentIndex + 1) % tabs.length];
+5 -2
View File
@@ -6,7 +6,7 @@ import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey
import type { ShortcutCombo } from '@/lib/shortcuts';
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
export type RightSidebarTab = 'git' | 'files';
export type RightSidebarTab = 'git' | 'files' | 'context';
export type ContextPanelMode = 'diff' | 'file' | 'context' | 'plan' | 'chat';
export type MermaidRenderingMode = 'svg' | 'ascii';
export type UserMessageRenderingMode = 'markdown' | 'plain';
@@ -1828,7 +1828,10 @@ export const useUIStore = create<UIStore>()(
delete state.memoryLimitActiveSession;
}
if (typeof state.rightSidebarTab !== 'string' || (state.rightSidebarTab !== 'git' && state.rightSidebarTab !== 'files')) {
if (
typeof state.rightSidebarTab !== 'string'
|| (state.rightSidebarTab !== 'git' && state.rightSidebarTab !== 'files' && state.rightSidebarTab !== 'context')
) {
state.rightSidebarTab = 'git';
}