|
1 | 1 | import { useParams, Link, useLocation } from "wouter"; |
2 | 2 | import { useProjectDetail } from "@/hooks/use-projects"; |
3 | 3 | import { useMarkdownContent } from "@/hooks/use-markdown"; |
4 | | -import { useSessions } from "@/hooks/use-sessions"; |
| 4 | +import { useSessions, useOpenSession } from "@/hooks/use-sessions"; |
5 | 5 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
6 | 6 | import { Badge } from "@/components/ui/badge"; |
7 | 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
8 | 8 | import { EntityBadge, entityConfig } from "@/components/entity-badge"; |
9 | 9 | import { Button } from "@/components/ui/button"; |
10 | | -import { ArrowLeft, FileText, Server, Wand2, HardDrive, MessageSquare, ExternalLink, Edit3, ChevronRight, Layers, Zap, Clock, Terminal, Code2, Copy, X, Check, Briefcase } from "lucide-react"; |
| 10 | +import { ArrowLeft, FileText, Server, Wand2, HardDrive, MessageSquare, ExternalLink, Edit3, ChevronRight, Layers, Zap, Clock, Terminal, Code2, Copy, X, Check, Briefcase, Loader2 } from "lucide-react"; |
11 | 11 | import type { MCPEntity, SkillEntity, MarkdownEntity, ScriptEntity } from "@shared/types"; |
12 | 12 | import { formatBytes, relativeTime } from "@/lib/utils"; |
13 | 13 | import { useEffect, useState } from "react"; |
@@ -63,6 +63,21 @@ export default function ProjectDetail() { |
63 | 63 | })(); |
64 | 64 | const inferredOnlyCount = projectSessions.filter(p => p.origin === "inferred").length; |
65 | 65 |
|
| 66 | + // One-click Resume — opens a new terminal in the session's recorded cwd |
| 67 | + // and runs `claude --resume <id>`. Same flow as the main Sessions page. |
| 68 | + const openSession = useOpenSession(); |
| 69 | + const [openState, setOpenState] = useState<{ id: string; phase: "opening" | "done" } | null>(null); |
| 70 | + const handleResume = (id: string, e: React.MouseEvent) => { |
| 71 | + e.stopPropagation(); |
| 72 | + setOpenState({ id, phase: "opening" }); |
| 73 | + openSession.mutate(id, { |
| 74 | + onSettled: () => { |
| 75 | + setOpenState({ id, phase: "done" }); |
| 76 | + setTimeout(() => setOpenState(prev => prev?.id === id ? null : prev), 2000); |
| 77 | + }, |
| 78 | + }); |
| 79 | + }; |
| 80 | + |
66 | 81 | return ( |
67 | 82 | <div className="p-6 space-y-6"> |
68 | 83 | {/* Breadcrumb */} |
@@ -348,6 +363,27 @@ export default function ProjectDetail() { |
348 | 363 | </span> |
349 | 364 | <span className="font-mono">{s.messageCount} msgs</span> |
350 | 365 | <span className="font-mono">{formatBytes(s.sizeBytes)}</span> |
| 366 | + <Button |
| 367 | + size="sm" |
| 368 | + variant="outline" |
| 369 | + className="gap-1.5 h-7" |
| 370 | + disabled={openState?.id === s.id && openState.phase === "opening"} |
| 371 | + onClick={(e) => handleResume(s.id, e)} |
| 372 | + title={`Open a terminal in ${s.cwd || "the session's directory"} and run claude --resume`} |
| 373 | + > |
| 374 | + {openState?.id === s.id && openState.phase === "opening" ? ( |
| 375 | + <Loader2 className="h-3.5 w-3.5 animate-spin text-green-400" /> |
| 376 | + ) : openState?.id === s.id && openState.phase === "done" ? ( |
| 377 | + <Check className="h-3.5 w-3.5 text-green-400" /> |
| 378 | + ) : ( |
| 379 | + <Terminal className="h-3.5 w-3.5 text-green-400" /> |
| 380 | + )} |
| 381 | + {openState?.id === s.id && openState.phase === "opening" |
| 382 | + ? "Opening…" |
| 383 | + : openState?.id === s.id && openState.phase === "done" |
| 384 | + ? "Opened" |
| 385 | + : "Resume"} |
| 386 | + </Button> |
351 | 387 | </div> |
352 | 388 | </div> |
353 | 389 | </CardContent> |
|
0 commit comments