Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 80 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus, Loader2, Bot, FolderCode } from "lucide-react";
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
import { OutputCacheProvider } from "@/lib/outputCache";
import { SessionProvider, useSessionContext } from "@/contexts/SessionContext";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ProjectList } from "@/components/ProjectList";
Expand All @@ -17,14 +18,17 @@ import { UsageDashboard } from "@/components/UsageDashboard";
import { MCPManager } from "@/components/MCPManager";
import { NFOCredits } from "@/components/NFOCredits";
import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog";
import { NavigationConfirmDialog } from "@/components/NavigationConfirmDialog";
import { ActiveClaudeSessions } from "@/components/ActiveClaudeSessions";
import { Toast, ToastContainer } from "@/components/ui/toast";

type View = "welcome" | "projects" | "agents" | "editor" | "settings" | "claude-file-editor" | "claude-code-session" | "usage-dashboard" | "mcp";

/**
* Main App component - Manages the Claude directory browser UI
* Inner App component that uses the session context
*/
function App() {
function AppInner() {
const { checkNavigationAllowed, activeSession } = useSessionContext();
const [view, setView] = useState<View>("welcome");
const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
Expand All @@ -36,6 +40,39 @@ function App() {
const [showNFO, setShowNFO] = useState(false);
const [showClaudeBinaryDialog, setShowClaudeBinaryDialog] = useState(false);
const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null);
const [pendingView, setPendingView] = useState<View | null>(null);
const [showNavConfirmDialog, setShowNavConfirmDialog] = useState(false);

// Navigation guard handler
const handleNavigation = useCallback(async (newView: View) => {
// Skip check if we're already on the same view or navigating to claude-code-session
if (view === newView || newView === "claude-code-session") {
setView(newView);
return;
}

// Check if navigation is allowed when leaving claude-code-session
if (view === "claude-code-session" && activeSession?.isActive) {
setPendingView(newView);
setShowNavConfirmDialog(true);
} else {
setView(newView);
}
}, [view, activeSession, checkNavigationAllowed]);

// Handle navigation confirmation
const handleConfirmNavigation = useCallback(() => {
if (pendingView) {
setView(pendingView);
setPendingView(null);
}
setShowNavConfirmDialog(false);
}, [pendingView]);

const handleCancelNavigation = useCallback(() => {
setPendingView(null);
setShowNavConfirmDialog(false);
}, []);

// Load projects on mount when in projects view
useEffect(() => {
Expand All @@ -52,7 +89,7 @@ function App() {
const handleSessionSelected = (event: CustomEvent) => {
const { session } = event.detail;
setSelectedSession(session);
setView("claude-code-session");
handleNavigation("claude-code-session");
};

const handleClaudeNotFound = () => {
Expand All @@ -65,7 +102,7 @@ function App() {
window.removeEventListener('claude-session-selected', handleSessionSelected as EventListener);
window.removeEventListener('claude-not-found', handleClaudeNotFound as EventListener);
};
}, []);
}, [handleNavigation]);

/**
* Loads all projects from the ~/.claude/projects directory
Expand Down Expand Up @@ -106,7 +143,7 @@ function App() {
* Opens a new Claude Code session in the interactive UI
*/
const handleNewSession = async () => {
setView("claude-code-session");
handleNavigation("claude-code-session");
setSelectedSession(null);
};

Expand All @@ -131,7 +168,7 @@ function App() {
*/
const handleBackFromClaudeFileEditor = () => {
setEditingClaudeFile(null);
setView("projects");
handleNavigation("projects");
};

const renderContent = () => {
Expand All @@ -153,6 +190,11 @@ function App() {
</h1>
</motion.div>

{/* Active Sessions */}
<div className="mb-8">
<ActiveClaudeSessions />
</div>

{/* Navigation Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-2xl mx-auto">
{/* CC Agents Card */}
Expand All @@ -163,7 +205,7 @@ function App() {
>
<Card
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover"
onClick={() => setView("agents")}
onClick={() => handleNavigation("agents")}
>
<div className="h-full flex flex-col items-center justify-center p-8">
<Bot className="h-16 w-16 mb-4 text-primary" />
Expand All @@ -180,7 +222,7 @@ function App() {
>
<Card
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover"
onClick={() => setView("projects")}
onClick={() => handleNavigation("projects")}
>
<div className="h-full flex flex-col items-center justify-center p-8">
<FolderCode className="h-16 w-16 mb-4 text-primary" />
Expand All @@ -197,21 +239,21 @@ function App() {
case "agents":
return (
<div className="flex-1 overflow-hidden">
<CCAgents onBack={() => setView("welcome")} />
<CCAgents onBack={() => handleNavigation("welcome")} />
</div>
);

case "editor":
return (
<div className="flex-1 overflow-hidden">
<MarkdownEditor onBack={() => setView("welcome")} />
<MarkdownEditor onBack={() => handleNavigation("welcome")} />
</div>
);

case "settings":
return (
<div className="flex-1 flex flex-col" style={{ minHeight: 0 }}>
<Settings onBack={() => setView("welcome")} />
<Settings onBack={() => handleNavigation("welcome")} />
</div>
);

Expand All @@ -229,7 +271,7 @@ function App() {
<Button
variant="ghost"
size="sm"
onClick={() => setView("welcome")}
onClick={() => handleNavigation("welcome")}
className="mb-4"
>
← Back to Home
Expand Down Expand Up @@ -338,19 +380,19 @@ function App() {
session={selectedSession || undefined}
onBack={() => {
setSelectedSession(null);
setView("projects");
handleNavigation("projects");
}}
/>
);

case "usage-dashboard":
return (
<UsageDashboard onBack={() => setView("welcome")} />
<UsageDashboard onBack={() => handleNavigation("welcome")} />
);

case "mcp":
return (
<MCPManager onBack={() => setView("welcome")} />
<MCPManager onBack={() => handleNavigation("welcome")} />
);

default:
Expand All @@ -363,10 +405,10 @@ function App() {
<div className="h-screen bg-background flex flex-col">
{/* Topbar */}
<Topbar
onClaudeClick={() => setView("editor")}
onSettingsClick={() => setView("settings")}
onUsageClick={() => setView("usage-dashboard")}
onMCPClick={() => setView("mcp")}
onClaudeClick={() => handleNavigation("editor")}
onSettingsClick={() => handleNavigation("settings")}
onUsageClick={() => handleNavigation("usage-dashboard")}
onMCPClick={() => handleNavigation("mcp")}
onInfoClick={() => setShowNFO(true)}
/>

Expand Down Expand Up @@ -400,9 +442,27 @@ function App() {
/>
)}
</ToastContainer>

{/* Navigation Confirmation Dialog */}
<NavigationConfirmDialog
open={showNavConfirmDialog}
onConfirm={handleConfirmNavigation}
onCancel={handleCancelNavigation}
/>
</div>
</OutputCacheProvider>
);
}

/**
* Main App component - Manages the Claude directory browser UI
*/
function App() {
return (
<SessionProvider>
<AppInner />
</SessionProvider>
);
}

export default App;
110 changes: 110 additions & 0 deletions src/components/ActiveClaudeSessions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MessageSquare, Play, Clock, ArrowRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useSessionContext } from '@/contexts/SessionContext';

interface ActiveClaudeSessionsProps {
className?: string;
onSessionSelect?: (sessionId: string) => void;
}

export function ActiveClaudeSessions({ className, onSessionSelect }: ActiveClaudeSessionsProps) {
const { activeSessions, resumeSession } = useSessionContext();

const formatTimeSince = (timestamp: number) => {
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);

if (hours > 0) {
return `${hours}h ${minutes % 60}m ago`;
}
return `${minutes}m ago`;
};

const handleResumeSession = (sessionId: string) => {
resumeSession(sessionId);
onSessionSelect?.(sessionId);
};

if (activeSessions.length === 0) {
return null;
}

return (
<div className={`space-y-4 ${className}`}>
<div className="flex items-center space-x-2">
<MessageSquare className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Active Claude Sessions</h3>
<Badge variant="secondary">{activeSessions.length}</Badge>
</div>

<div className="space-y-3">
<AnimatePresence>
{activeSessions.map((session) => (
<motion.div
key={session.sessionId}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<Card className="hover:shadow-md transition-shadow cursor-pointer group">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center w-8 h-8 bg-primary/10 rounded-full">
<MessageSquare className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-base">Claude Code Session</CardTitle>
<div className="flex items-center space-x-2 mt-1">
<Badge variant="default" className="bg-green-100 text-green-800 border-green-200">
<Play className="h-3 w-3 mr-1" />
Active
</Badge>
<Badge variant="outline" className="text-xs">
<Clock className="h-3 w-3 mr-1" />
{formatTimeSince(session.timestamp)}
</Badge>
</div>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleResumeSession(session.sessionId)}
className="flex items-center space-x-2 group-hover:bg-primary group-hover:text-primary-foreground transition-colors"
>
<span>Resume</span>
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
<div>
<p className="text-sm text-muted-foreground">Project Path</p>
<p className="text-xs font-mono bg-muted px-2 py-1 rounded truncate">
{session.projectPath}
</p>
</div>

<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="text-sm">{session.messages.length} messages in conversation</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
);
}
Loading
Loading