mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Implement initial project structure and features for Automaker application, including environment setup, auto mode services, and session management. Update port configurations to 3007 and add new UI components for enhanced user interaction.
This commit is contained in:
367
app/src/components/session-manager.tsx
Normal file
367
app/src/components/session-manager.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import {
|
||||
Plus,
|
||||
MessageSquare,
|
||||
Archive,
|
||||
Trash2,
|
||||
Edit2,
|
||||
Check,
|
||||
X,
|
||||
ArchiveRestore,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SessionListItem } from "@/types/electron";
|
||||
|
||||
interface SessionManagerProps {
|
||||
currentSessionId: string | null;
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
projectPath: string;
|
||||
isCurrentSessionThinking?: boolean;
|
||||
}
|
||||
|
||||
export function SessionManager({
|
||||
currentSessionId,
|
||||
onSelectSession,
|
||||
projectPath,
|
||||
isCurrentSessionThinking = false,
|
||||
}: SessionManagerProps) {
|
||||
const [sessions, setSessions] = useState<SessionListItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"active" | "archived">("active");
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newSessionName, setNewSessionName] = useState("");
|
||||
|
||||
// Load sessions
|
||||
const loadSessions = async () => {
|
||||
if (!window.electronAPI?.sessions) return;
|
||||
|
||||
// Always load all sessions and filter client-side
|
||||
const result = await window.electronAPI.sessions.list(true);
|
||||
if (result.success && result.sessions) {
|
||||
setSessions(result.sessions);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, []);
|
||||
|
||||
// Create new session
|
||||
const handleCreateSession = async () => {
|
||||
if (!newSessionName.trim() || !window.electronAPI?.sessions) return;
|
||||
|
||||
const result = await window.electronAPI.sessions.create(
|
||||
newSessionName,
|
||||
projectPath,
|
||||
projectPath
|
||||
);
|
||||
|
||||
if (result.success && result.sessionId) {
|
||||
setNewSessionName("");
|
||||
setIsCreating(false);
|
||||
await loadSessions();
|
||||
onSelectSession(result.sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
// Rename session
|
||||
const handleRenameSession = async (sessionId: string) => {
|
||||
if (!editingName.trim() || !window.electronAPI?.sessions) return;
|
||||
|
||||
const result = await window.electronAPI.sessions.update(
|
||||
sessionId,
|
||||
editingName,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
await loadSessions();
|
||||
}
|
||||
};
|
||||
|
||||
// Archive session
|
||||
const handleArchiveSession = async (sessionId: string) => {
|
||||
if (!window.electronAPI?.sessions) return;
|
||||
|
||||
const result = await window.electronAPI.sessions.archive(sessionId);
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
}
|
||||
};
|
||||
|
||||
// Unarchive session
|
||||
const handleUnarchiveSession = async (sessionId: string) => {
|
||||
if (!window.electronAPI?.sessions) return;
|
||||
|
||||
const result = await window.electronAPI.sessions.unarchive(sessionId);
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete session
|
||||
const handleDeleteSession = async (sessionId: string) => {
|
||||
if (!window.electronAPI?.sessions) return;
|
||||
if (!confirm("Are you sure you want to delete this session?")) return;
|
||||
|
||||
const result = await window.electronAPI.sessions.delete(sessionId);
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
if (currentSessionId === sessionId) {
|
||||
// Switch to another session or create a new one
|
||||
const activeSessionsList = sessions.filter((s) => !s.isArchived);
|
||||
if (activeSessionsList.length > 0) {
|
||||
onSelectSession(activeSessionsList[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activeSessions = sessions.filter((s) => !s.isArchived);
|
||||
const archivedSessions = sessions.filter((s) => s.isArchived);
|
||||
const displayedSessions = activeTab === "active" ? activeSessions : archivedSessions;
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<CardTitle>Agent Sessions</CardTitle>
|
||||
{activeTab === "active" && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => setIsCreating(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
New
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as "active" | "archived")}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="active" className="flex-1">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Active ({activeSessions.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="archived" className="flex-1">
|
||||
<Archive className="w-4 h-4 mr-2" />
|
||||
Archived ({archivedSessions.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 overflow-y-auto space-y-2">
|
||||
{/* Create new session */}
|
||||
{isCreating && (
|
||||
<div className="p-3 border rounded-lg bg-muted/50">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Session name..."
|
||||
value={newSessionName}
|
||||
onChange={(e) => setNewSessionName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreateSession();
|
||||
if (e.key === "Escape") {
|
||||
setIsCreating(false);
|
||||
setNewSessionName("");
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" onClick={handleCreateSession}>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsCreating(false);
|
||||
setNewSessionName("");
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session list */}
|
||||
{displayedSessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={cn(
|
||||
"p-3 border rounded-lg cursor-pointer transition-colors hover:bg-accent/50",
|
||||
currentSessionId === session.id && "bg-primary/10 border-primary",
|
||||
session.isArchived && "opacity-60"
|
||||
)}
|
||||
onClick={() => !session.isArchived && onSelectSession(session.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
{editingSessionId === session.id ? (
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter")
|
||||
handleRenameSession(session.id);
|
||||
if (e.key === "Escape") {
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
autoFocus
|
||||
className="h-7"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRenameSession(session.id);
|
||||
}}
|
||||
className="h-7"
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
}}
|
||||
className="h-7"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{currentSessionId === session.id && isCurrentSessionThinking ? (
|
||||
<Loader2 className="w-4 h-4 text-primary animate-spin shrink-0" />
|
||||
) : (
|
||||
<MessageSquare className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<h3 className="font-medium truncate">{session.name}</h3>
|
||||
{currentSessionId === session.id && isCurrentSessionThinking && (
|
||||
<span className="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
|
||||
thinking...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{session.preview && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{session.preview}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{session.messageCount} messages
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">·</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(session.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{!session.isArchived && (
|
||||
<div
|
||||
className="flex gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setEditingSessionId(session.id);
|
||||
setEditingName(session.name);
|
||||
}}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleArchiveSession(session.id)}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<Archive className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session.isArchived && (
|
||||
<div
|
||||
className="flex gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleUnarchiveSession(session.id)}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ArchiveRestore className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteSession(session.id)}
|
||||
className="h-7 w-7 p-0 text-destructive"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{displayedSessions.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{activeTab === "active" ? "No active sessions" : "No archived sessions"}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{activeTab === "active"
|
||||
? "Create your first session to get started"
|
||||
: "Archive sessions to see them here"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user