Skip to content

Commit 660db91

Browse files
feat: add navigation guards and session persistence for Claude Code
- Add global session context to track active Claude Code sessions - Implement navigation confirmation dialog when leaving active sessions - Add session persistence to localStorage with 24-hour expiry - Create Active Sessions UI component on welcome page - Enable resuming interrupted sessions with full conversation history - Prevent accidental loss of Claude conversations Fixes #50 Co-authored-by: Mufeed VH <mufeedvh@users.noreply.github.com>
1 parent 4fb6fd5 commit 660db91

6 files changed

Lines changed: 490 additions & 21 deletions

File tree

src/App.tsx

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useCallback } from "react";
22
import { motion, AnimatePresence } from "framer-motion";
33
import { Plus, Loader2, Bot, FolderCode } from "lucide-react";
44
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
55
import { OutputCacheProvider } from "@/lib/outputCache";
6+
import { SessionProvider, useSessionContext } from "@/contexts/SessionContext";
67
import { Button } from "@/components/ui/button";
78
import { Card } from "@/components/ui/card";
89
import { ProjectList } from "@/components/ProjectList";
@@ -17,14 +18,17 @@ import { UsageDashboard } from "@/components/UsageDashboard";
1718
import { MCPManager } from "@/components/MCPManager";
1819
import { NFOCredits } from "@/components/NFOCredits";
1920
import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog";
21+
import { NavigationConfirmDialog } from "@/components/NavigationConfirmDialog";
22+
import { ActiveClaudeSessions } from "@/components/ActiveClaudeSessions";
2023
import { Toast, ToastContainer } from "@/components/ui/toast";
2124

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

2427
/**
25-
* Main App component - Manages the Claude directory browser UI
28+
* Inner App component that uses the session context
2629
*/
27-
function App() {
30+
function AppInner() {
31+
const { checkNavigationAllowed, activeSession } = useSessionContext();
2832
const [view, setView] = useState<View>("welcome");
2933
const [projects, setProjects] = useState<Project[]>([]);
3034
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
@@ -36,6 +40,39 @@ function App() {
3640
const [showNFO, setShowNFO] = useState(false);
3741
const [showClaudeBinaryDialog, setShowClaudeBinaryDialog] = useState(false);
3842
const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null);
43+
const [pendingView, setPendingView] = useState<View | null>(null);
44+
const [showNavConfirmDialog, setShowNavConfirmDialog] = useState(false);
45+
46+
// Navigation guard handler
47+
const handleNavigation = useCallback(async (newView: View) => {
48+
// Skip check if we're already on the same view or navigating to claude-code-session
49+
if (view === newView || newView === "claude-code-session") {
50+
setView(newView);
51+
return;
52+
}
53+
54+
// Check if navigation is allowed when leaving claude-code-session
55+
if (view === "claude-code-session" && activeSession?.isActive) {
56+
setPendingView(newView);
57+
setShowNavConfirmDialog(true);
58+
} else {
59+
setView(newView);
60+
}
61+
}, [view, activeSession, checkNavigationAllowed]);
62+
63+
// Handle navigation confirmation
64+
const handleConfirmNavigation = useCallback(() => {
65+
if (pendingView) {
66+
setView(pendingView);
67+
setPendingView(null);
68+
}
69+
setShowNavConfirmDialog(false);
70+
}, [pendingView]);
71+
72+
const handleCancelNavigation = useCallback(() => {
73+
setPendingView(null);
74+
setShowNavConfirmDialog(false);
75+
}, []);
3976

4077
// Load projects on mount when in projects view
4178
useEffect(() => {
@@ -52,7 +89,7 @@ function App() {
5289
const handleSessionSelected = (event: CustomEvent) => {
5390
const { session } = event.detail;
5491
setSelectedSession(session);
55-
setView("claude-code-session");
92+
handleNavigation("claude-code-session");
5693
};
5794

5895
const handleClaudeNotFound = () => {
@@ -65,7 +102,7 @@ function App() {
65102
window.removeEventListener('claude-session-selected', handleSessionSelected as EventListener);
66103
window.removeEventListener('claude-not-found', handleClaudeNotFound as EventListener);
67104
};
68-
}, []);
105+
}, [handleNavigation]);
69106

70107
/**
71108
* Loads all projects from the ~/.claude/projects directory
@@ -106,7 +143,7 @@ function App() {
106143
* Opens a new Claude Code session in the interactive UI
107144
*/
108145
const handleNewSession = async () => {
109-
setView("claude-code-session");
146+
handleNavigation("claude-code-session");
110147
setSelectedSession(null);
111148
};
112149

@@ -131,7 +168,7 @@ function App() {
131168
*/
132169
const handleBackFromClaudeFileEditor = () => {
133170
setEditingClaudeFile(null);
134-
setView("projects");
171+
handleNavigation("projects");
135172
};
136173

137174
const renderContent = () => {
@@ -153,6 +190,11 @@ function App() {
153190
</h1>
154191
</motion.div>
155192

193+
{/* Active Sessions */}
194+
<div className="mb-8">
195+
<ActiveClaudeSessions />
196+
</div>
197+
156198
{/* Navigation Cards */}
157199
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-2xl mx-auto">
158200
{/* CC Agents Card */}
@@ -163,7 +205,7 @@ function App() {
163205
>
164206
<Card
165207
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover"
166-
onClick={() => setView("agents")}
208+
onClick={() => handleNavigation("agents")}
167209
>
168210
<div className="h-full flex flex-col items-center justify-center p-8">
169211
<Bot className="h-16 w-16 mb-4 text-primary" />
@@ -180,7 +222,7 @@ function App() {
180222
>
181223
<Card
182224
className="h-64 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-lg border border-border/50 shimmer-hover"
183-
onClick={() => setView("projects")}
225+
onClick={() => handleNavigation("projects")}
184226
>
185227
<div className="h-full flex flex-col items-center justify-center p-8">
186228
<FolderCode className="h-16 w-16 mb-4 text-primary" />
@@ -197,21 +239,21 @@ function App() {
197239
case "agents":
198240
return (
199241
<div className="flex-1 overflow-hidden">
200-
<CCAgents onBack={() => setView("welcome")} />
242+
<CCAgents onBack={() => handleNavigation("welcome")} />
201243
</div>
202244
);
203245

204246
case "editor":
205247
return (
206248
<div className="flex-1 overflow-hidden">
207-
<MarkdownEditor onBack={() => setView("welcome")} />
249+
<MarkdownEditor onBack={() => handleNavigation("welcome")} />
208250
</div>
209251
);
210252

211253
case "settings":
212254
return (
213255
<div className="flex-1 flex flex-col" style={{ minHeight: 0 }}>
214-
<Settings onBack={() => setView("welcome")} />
256+
<Settings onBack={() => handleNavigation("welcome")} />
215257
</div>
216258
);
217259

@@ -229,7 +271,7 @@ function App() {
229271
<Button
230272
variant="ghost"
231273
size="sm"
232-
onClick={() => setView("welcome")}
274+
onClick={() => handleNavigation("welcome")}
233275
className="mb-4"
234276
>
235277
← Back to Home
@@ -338,19 +380,19 @@ function App() {
338380
session={selectedSession || undefined}
339381
onBack={() => {
340382
setSelectedSession(null);
341-
setView("projects");
383+
handleNavigation("projects");
342384
}}
343385
/>
344386
);
345387

346388
case "usage-dashboard":
347389
return (
348-
<UsageDashboard onBack={() => setView("welcome")} />
390+
<UsageDashboard onBack={() => handleNavigation("welcome")} />
349391
);
350392

351393
case "mcp":
352394
return (
353-
<MCPManager onBack={() => setView("welcome")} />
395+
<MCPManager onBack={() => handleNavigation("welcome")} />
354396
);
355397

356398
default:
@@ -363,10 +405,10 @@ function App() {
363405
<div className="h-screen bg-background flex flex-col">
364406
{/* Topbar */}
365407
<Topbar
366-
onClaudeClick={() => setView("editor")}
367-
onSettingsClick={() => setView("settings")}
368-
onUsageClick={() => setView("usage-dashboard")}
369-
onMCPClick={() => setView("mcp")}
408+
onClaudeClick={() => handleNavigation("editor")}
409+
onSettingsClick={() => handleNavigation("settings")}
410+
onUsageClick={() => handleNavigation("usage-dashboard")}
411+
onMCPClick={() => handleNavigation("mcp")}
370412
onInfoClick={() => setShowNFO(true)}
371413
/>
372414

@@ -400,9 +442,27 @@ function App() {
400442
/>
401443
)}
402444
</ToastContainer>
445+
446+
{/* Navigation Confirmation Dialog */}
447+
<NavigationConfirmDialog
448+
open={showNavConfirmDialog}
449+
onConfirm={handleConfirmNavigation}
450+
onCancel={handleCancelNavigation}
451+
/>
403452
</div>
404453
</OutputCacheProvider>
405454
);
406455
}
407456

457+
/**
458+
* Main App component - Manages the Claude directory browser UI
459+
*/
460+
function App() {
461+
return (
462+
<SessionProvider>
463+
<AppInner />
464+
</SessionProvider>
465+
);
466+
}
467+
408468
export default App;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { MessageSquare, Play, Clock, ArrowRight } from 'lucide-react';
4+
import { Button } from '@/components/ui/button';
5+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6+
import { Badge } from '@/components/ui/badge';
7+
import { useSessionContext } from '@/contexts/SessionContext';
8+
9+
interface ActiveClaudeSessionsProps {
10+
className?: string;
11+
onSessionSelect?: (sessionId: string) => void;
12+
}
13+
14+
export function ActiveClaudeSessions({ className, onSessionSelect }: ActiveClaudeSessionsProps) {
15+
const { activeSessions, resumeSession } = useSessionContext();
16+
17+
const formatTimeSince = (timestamp: number) => {
18+
const now = Date.now();
19+
const diff = now - timestamp;
20+
const minutes = Math.floor(diff / (1000 * 60));
21+
const hours = Math.floor(minutes / 60);
22+
23+
if (hours > 0) {
24+
return `${hours}h ${minutes % 60}m ago`;
25+
}
26+
return `${minutes}m ago`;
27+
};
28+
29+
const handleResumeSession = (sessionId: string) => {
30+
resumeSession(sessionId);
31+
onSessionSelect?.(sessionId);
32+
};
33+
34+
if (activeSessions.length === 0) {
35+
return null;
36+
}
37+
38+
return (
39+
<div className={`space-y-4 ${className}`}>
40+
<div className="flex items-center space-x-2">
41+
<MessageSquare className="h-5 w-5 text-primary" />
42+
<h3 className="text-lg font-semibold">Active Claude Sessions</h3>
43+
<Badge variant="secondary">{activeSessions.length}</Badge>
44+
</div>
45+
46+
<div className="space-y-3">
47+
<AnimatePresence>
48+
{activeSessions.map((session) => (
49+
<motion.div
50+
key={session.sessionId}
51+
initial={{ opacity: 0, y: 20 }}
52+
animate={{ opacity: 1, y: 0 }}
53+
exit={{ opacity: 0, y: -20 }}
54+
transition={{ duration: 0.2 }}
55+
>
56+
<Card className="hover:shadow-md transition-shadow cursor-pointer group">
57+
<CardHeader className="pb-3">
58+
<div className="flex items-center justify-between">
59+
<div className="flex items-center space-x-3">
60+
<div className="flex items-center justify-center w-8 h-8 bg-primary/10 rounded-full">
61+
<MessageSquare className="h-5 w-5 text-primary" />
62+
</div>
63+
<div>
64+
<CardTitle className="text-base">Claude Code Session</CardTitle>
65+
<div className="flex items-center space-x-2 mt-1">
66+
<Badge variant="default" className="bg-green-100 text-green-800 border-green-200">
67+
<Play className="h-3 w-3 mr-1" />
68+
Active
69+
</Badge>
70+
<Badge variant="outline" className="text-xs">
71+
<Clock className="h-3 w-3 mr-1" />
72+
{formatTimeSince(session.timestamp)}
73+
</Badge>
74+
</div>
75+
</div>
76+
</div>
77+
<Button
78+
variant="outline"
79+
size="sm"
80+
onClick={() => handleResumeSession(session.sessionId)}
81+
className="flex items-center space-x-2 group-hover:bg-primary group-hover:text-primary-foreground transition-colors"
82+
>
83+
<span>Resume</span>
84+
<ArrowRight className="h-4 w-4" />
85+
</Button>
86+
</div>
87+
</CardHeader>
88+
<CardContent className="pt-0">
89+
<div className="space-y-2">
90+
<div>
91+
<p className="text-sm text-muted-foreground">Project Path</p>
92+
<p className="text-xs font-mono bg-muted px-2 py-1 rounded truncate">
93+
{session.projectPath}
94+
</p>
95+
</div>
96+
97+
<div>
98+
<p className="text-sm text-muted-foreground">Messages</p>
99+
<p className="text-sm">{session.messages.length} messages in conversation</p>
100+
</div>
101+
</div>
102+
</CardContent>
103+
</Card>
104+
</motion.div>
105+
))}
106+
</AnimatePresence>
107+
</div>
108+
</div>
109+
);
110+
}

0 commit comments

Comments
 (0)