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:
Bohdan Triapitsyn
2026-01-15 20:19:06 +02:00
committed by GitHub
parent ecf81c901d
commit 1be5dfda05
21 changed files with 2070 additions and 82 deletions
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';