feat(context-panel): add plan view to sidebar panel (#446)
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { RiCloseLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
|
import { RiCloseLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DiffView, FilesView } from '@/components/views';
|
import { DiffView, FilesView, PlanView } from '@/components/views';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
@@ -145,7 +145,7 @@ export const ContextPanel: React.FC = () => {
|
|||||||
|
|
||||||
const activeFilePath = useFilesViewTabsStore((state) => (directoryKey ? (state.byRoot[directoryKey]?.selectedPath ?? null) : null));
|
const activeFilePath = useFilesViewTabsStore((state) => (directoryKey ? (state.byRoot[directoryKey]?.selectedPath ?? null) : null));
|
||||||
|
|
||||||
const panelTitle = panelState?.mode === 'diff' ? 'Diff' : panelState?.mode === 'file' ? 'File' : panelState?.mode === 'context' ? 'Context' : 'Panel';
|
const panelTitle = panelState?.mode === 'diff' ? 'Diff' : panelState?.mode === 'file' ? 'File' : panelState?.mode === 'context' ? 'Context' : panelState?.mode === 'plan' ? 'Plan' : 'Panel';
|
||||||
const effectivePath = panelState?.mode === 'file' ? (activeFilePath ?? panelState?.targetPath ?? null) : panelState?.mode === 'context' ? null : (panelState?.targetPath ?? null);
|
const effectivePath = panelState?.mode === 'file' ? (activeFilePath ?? panelState?.targetPath ?? null) : panelState?.mode === 'context' ? null : (panelState?.targetPath ?? null);
|
||||||
const pathLabel = getRelativePathLabel(effectivePath, effectiveDirectory);
|
const pathLabel = getRelativePathLabel(effectivePath, effectiveDirectory);
|
||||||
|
|
||||||
@@ -155,7 +155,9 @@ export const ContextPanel: React.FC = () => {
|
|||||||
? <FilesView mode="editor-only" />
|
? <FilesView mode="editor-only" />
|
||||||
: panelState?.mode === 'context'
|
: panelState?.mode === 'context'
|
||||||
? <ContextPanelContent />
|
? <ContextPanelContent />
|
||||||
: null;
|
: panelState?.mode === 'plan'
|
||||||
|
? <PlanView />
|
||||||
|
: null;
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<header className="flex h-10 items-center gap-2 border-b border-border/40 px-2.5">
|
<header className="flex h-10 items-center gap-2 border-b border-border/40 px-2.5">
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export const Header: React.FC = () => {
|
|||||||
const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal);
|
const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal);
|
||||||
const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar);
|
const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar);
|
||||||
const openContextOverview = useUIStore((state) => state.openContextOverview);
|
const openContextOverview = useUIStore((state) => state.openContextOverview);
|
||||||
|
const openContextPlan = useUIStore((state) => state.openContextPlan);
|
||||||
const closeContextPanel = useUIStore((state) => state.closeContextPanel);
|
const closeContextPanel = useUIStore((state) => state.closeContextPanel);
|
||||||
const contextPanelByDirectory = useUIStore((state) => state.contextPanelByDirectory);
|
const contextPanelByDirectory = useUIStore((state) => state.contextPanelByDirectory);
|
||||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||||
@@ -917,6 +918,30 @@ export const Header: React.FC = () => {
|
|||||||
return Boolean(panelState?.isOpen && panelState.mode === 'context');
|
return Boolean(panelState?.isOpen && panelState.mode === 'context');
|
||||||
}, [contextPanelByDirectory, openDirectory]);
|
}, [contextPanelByDirectory, openDirectory]);
|
||||||
|
|
||||||
|
const handleOpenContextPlan = React.useCallback(() => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
if (panelState?.isOpen && panelState.mode === 'plan') {
|
||||||
|
closeContextPanel(directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openContextPlan(directory);
|
||||||
|
}, [closeContextPanel, contextPanelByDirectory, openContextPlan, openDirectory]);
|
||||||
|
|
||||||
|
const isContextPlanActive = React.useMemo(() => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
return Boolean(panelState?.isOpen && panelState.mode === 'plan');
|
||||||
|
}, [contextPanelByDirectory, openDirectory]);
|
||||||
|
|
||||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
|
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
|
||||||
|
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
@@ -1644,6 +1669,23 @@ export const Header: React.FC = () => {
|
|||||||
percentIconClassName="h-5 w-5"
|
percentIconClassName="h-5 w-5"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showPlanTab && (
|
||||||
|
<Tooltip delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Open plan"
|
||||||
|
onClick={handleOpenContextPlan}
|
||||||
|
className={cn(headerIconButtonClass, isContextPlanActive && 'bg-[var(--interactive-hover)] text-foreground')}
|
||||||
|
>
|
||||||
|
<RiFileTextLine className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Plan</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<OpenInAppButton directory={openDirectory} className="mr-1" />
|
<OpenInAppButton directory={openDirectory} className="mr-1" />
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={isDesktopServicesOpen}
|
open={isDesktopServicesOpen}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey
|
|||||||
|
|
||||||
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
|
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
|
||||||
export type RightSidebarTab = 'git' | 'files';
|
export type RightSidebarTab = 'git' | 'files';
|
||||||
export type ContextPanelMode = 'diff' | 'file' | 'context';
|
export type ContextPanelMode = 'diff' | 'file' | 'context' | 'plan';
|
||||||
|
|
||||||
type ContextPanelDirectoryState = {
|
type ContextPanelDirectoryState = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -225,6 +225,7 @@ interface UIStore {
|
|||||||
openContextDiff: (directory: string, filePath: string) => void;
|
openContextDiff: (directory: string, filePath: string) => void;
|
||||||
openContextFile: (directory: string, filePath: string) => void;
|
openContextFile: (directory: string, filePath: string) => void;
|
||||||
openContextOverview: (directory: string) => void;
|
openContextOverview: (directory: string) => void;
|
||||||
|
openContextPlan: (directory: string) => void;
|
||||||
closeContextPanel: (directory: string) => void;
|
closeContextPanel: (directory: string) => void;
|
||||||
toggleContextPanelExpanded: (directory: string) => void;
|
toggleContextPanelExpanded: (directory: string) => void;
|
||||||
setContextPanelWidth: (directory: string, width: number) => void;
|
setContextPanelWidth: (directory: string, width: number) => void;
|
||||||
@@ -549,6 +550,29 @@ export const useUIStore = create<UIStore>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openContextPlan: (directory) => {
|
||||||
|
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||||
|
if (!normalizedDirectory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||||
|
const current = touchContextPanelState(prev);
|
||||||
|
const byDirectory = {
|
||||||
|
...state.contextPanelByDirectory,
|
||||||
|
[normalizedDirectory]: {
|
||||||
|
...current,
|
||||||
|
isOpen: true,
|
||||||
|
mode: 'plan' as const,
|
||||||
|
targetPath: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
closeContextPanel: (directory) => {
|
closeContextPanel: (directory) => {
|
||||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||||
if (!normalizedDirectory) {
|
if (!normalizedDirectory) {
|
||||||
|
|||||||
Reference in New Issue
Block a user