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:
@@ -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];
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user