feat: add Files tab for browsing workspace files (#154)
* feat: add Files tab for browsing workspace files - Add Files tab between Diff and Terminal in header - Implement hierarchical file tree with expand/collapse - Add fuzzy search with debouncing and relevance ranking - Support gitignore filtering via `git check-ignore` (web + desktop) - Add syntax highlighting for 150+ file types - Add image preview (SVG, PNG, JPG, etc.) - Add line numbers, wrap toggle, and copy button - Desktop: split-pane layout matching DiffView - Mobile: drill-in navigation with full-width sidebar - Update help dialog with Cmd+3 shortcut - Increase header breakpoint to 940px for new tab * feat: enhance context and session stores to track agent/model/variant choices for historical sessions * feat: implement line selection and commenting functionality in FilesView
This commit is contained in:
committed by
GitHub
parent
ecf81c901d
commit
1be5dfda05
@@ -5,7 +5,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
import { RiChat4Line, RiCodeLine, RiCommandLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { RiChat4Line, RiCodeLine, RiCommandLine, RiFolder6Line, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { useUIStore, type MainTab } from '@/stores/useUIStore';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
@@ -266,6 +266,7 @@ export const Header: React.FC = () => {
|
||||
icon: RiCodeLine,
|
||||
badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined,
|
||||
},
|
||||
{ id: 'files', label: 'Files', icon: RiFolder6Line },
|
||||
{ id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine },
|
||||
{
|
||||
id: 'git',
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useDeviceInfo } from '@/lib/device';
|
||||
import { useEdgeSwipe } from '@/hooks/useEdgeSwipe';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { ChatView, GitView, DiffView, TerminalView, SettingsView } from '@/components/views';
|
||||
import { ChatView, GitView, DiffView, TerminalView, FilesView, SettingsView } from '@/components/views';
|
||||
|
||||
export const MainLayout: React.FC = () => {
|
||||
const {
|
||||
@@ -306,6 +306,8 @@ export const MainLayout: React.FC = () => {
|
||||
return <DiffView />;
|
||||
case 'terminal':
|
||||
return <TerminalView />;
|
||||
case 'files':
|
||||
return <FilesView />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
RiCloseCircleLine,
|
||||
RiCodeLine,
|
||||
RiCommandLine,
|
||||
RiFolder6Line,
|
||||
RiGitBranchLine,
|
||||
RiLayoutLeftLine,
|
||||
RiPaletteLine,
|
||||
@@ -161,11 +162,16 @@ export const HelpDialog: React.FC = () => {
|
||||
},
|
||||
{
|
||||
keys: [`${mod} + 3`],
|
||||
description: "Open Files",
|
||||
icon: RiFolder6Line,
|
||||
},
|
||||
{
|
||||
keys: [`${mod} + 4`],
|
||||
description: "Open Terminal",
|
||||
icon: RiTerminalBoxLine,
|
||||
},
|
||||
{
|
||||
keys: [`${mod} + 4`],
|
||||
keys: [`${mod} + 5`],
|
||||
description: "Open Git Panel",
|
||||
icon: RiGitBranchLine,
|
||||
},
|
||||
|
||||
@@ -9,16 +9,18 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',
|
||||
className
|
||||
)}
|
||||
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
||||
'pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
||||
)}
|
||||
style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,11 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
||||
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
||||
|
||||
import { toast } from 'sonner';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useContextStore } from '@/stores/contextStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { cn, getModifierLabel } from '@/lib/utils';
|
||||
@@ -158,6 +160,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
const sendMessage = useSessionStore(state => state.sendMessage);
|
||||
const currentSessionId = useSessionStore(state => state.currentSessionId);
|
||||
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore();
|
||||
const getSessionAgentSelection = useContextStore(state => state.getSessionAgentSelection);
|
||||
const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession);
|
||||
const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession);
|
||||
|
||||
// Update main content center on resize
|
||||
useEffect(() => {
|
||||
@@ -210,6 +215,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
// Check if click is inside the comment UI portal
|
||||
const commentUI = document.querySelector('[data-comment-ui]');
|
||||
if (commentUI?.contains(target)) return;
|
||||
|
||||
// Check if click is inside toast (sonner)
|
||||
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
||||
|
||||
// Check if click is on a line number (inside shadow DOM)
|
||||
const path = e.composedPath();
|
||||
@@ -239,10 +247,25 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
|
||||
const handleSendComment = useCallback(async () => {
|
||||
if (!selection || !commentText.trim()) return;
|
||||
if (!currentSessionId || !currentProviderId || !currentModelId) {
|
||||
console.warn('Cannot send comment: no active session or model not selected');
|
||||
if (!currentSessionId) {
|
||||
toast.error('Select a session to send comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get session-specific agent/model/variant with fallback to config values
|
||||
const sessionAgent = getSessionAgentSelection(currentSessionId) || currentAgentName;
|
||||
const sessionModel = sessionAgent ? getAgentModelForSession(currentSessionId, sessionAgent) : null;
|
||||
const effectiveProviderId = sessionModel?.providerId || currentProviderId;
|
||||
const effectiveModelId = sessionModel?.modelId || currentModelId;
|
||||
|
||||
if (!effectiveProviderId || !effectiveModelId) {
|
||||
toast.error('Select a model to send comment');
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveVariant = sessionAgent && effectiveProviderId && effectiveModelId
|
||||
? getAgentModelVariantForSession(currentSessionId, sessionAgent, effectiveProviderId, effectiveModelId) ?? currentVariant
|
||||
: currentVariant;
|
||||
|
||||
const code = extractSelectedCode(original, modified, selection);
|
||||
const startLine = selection.start;
|
||||
@@ -259,18 +282,18 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
try {
|
||||
await sendMessage(
|
||||
message,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentAgentName,
|
||||
effectiveProviderId,
|
||||
effectiveModelId,
|
||||
sessionAgent,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
currentVariant
|
||||
effectiveVariant
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to send comment", e);
|
||||
}
|
||||
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab]);
|
||||
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]);
|
||||
|
||||
ensureFlexokiThemesRegistered();
|
||||
|
||||
@@ -339,7 +362,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center gap-2 px-4"
|
||||
style={{ width: 'min(100vw - 2rem, 42rem)' }}
|
||||
style={{ width: 'min(100vw - 1rem, 42rem)' }}
|
||||
>
|
||||
<div className="w-full rounded-xl border bg-sidebar flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
|
||||
{/* Textarea - auto-grows from 1 line to max 5 lines */}
|
||||
|
||||
@@ -45,7 +45,7 @@ const SETTINGS_SIDEBAR_MAX_WIDTH = 500;
|
||||
const SETTINGS_SIDEBAR_DEFAULT_WIDTH = 264;
|
||||
|
||||
// Width threshold for hiding tab labels (show icons only)
|
||||
const TAB_LABELS_MIN_WIDTH = 700;
|
||||
const TAB_LABELS_MIN_WIDTH = 940;
|
||||
|
||||
interface SettingsViewProps {
|
||||
onClose?: () => void;
|
||||
|
||||
@@ -2,4 +2,5 @@ export { ChatView } from './ChatView';
|
||||
export { GitView } from './GitView';
|
||||
export { DiffView, useDiffFileCount } from './DiffView';
|
||||
export { TerminalView } from './TerminalView';
|
||||
export { FilesView } from './FilesView';
|
||||
export { SettingsView } from './SettingsView';
|
||||
|
||||
Reference in New Issue
Block a user