sync: Auto-sync from DESKTOP-0O8A1RL at 2026-04-02 19:20:43

Synced files:
- Session logs updated
- Latest context and credentials
- Command/directive updates

Machine: DESKTOP-0O8A1RL
Timestamp: 2026-04-02 19:20:43

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 19:20:43 -07:00
parent 6e4ebc2db9
commit bff7d9dbbf
8 changed files with 981 additions and 27 deletions

View File

@@ -113,7 +113,7 @@ export interface Command {
agent_id: string;
command_type: string;
command_text: string;
status: "pending" | "running" | "completed" | "failed";
status: "pending" | "running" | "completed" | "failed" | "cancelled";
exit_code: number | null;
stdout: string | null;
stderr: string | null;
@@ -219,6 +219,11 @@ export const commandsApi = {
api.post<Command>(`/api/agents/${agentId}/command`, command),
list: () => api.get<Command[]>("/api/commands"),
get: (id: string) => api.get<Command>(`/api/commands/${id}`),
cancelCommand: (id: string) =>
api.post<{ status: string; message: string }>(`/api/commands/${id}/cancel`),
deleteCommand: (id: string) => api.delete(`/api/commands/${id}`),
clearCommandHistory: () =>
api.delete<{ deleted: number; message: string }>("/api/commands"),
};
export const clientsApi = {

View File

@@ -1,6 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link, useParams, useNavigate } from "react-router-dom";
import { RefreshCw, CheckCircle, XCircle, Clock, Loader2, ArrowLeft, Terminal } from "lucide-react";
import {
RefreshCw,
CheckCircle,
XCircle,
Clock,
Loader2,
ArrowLeft,
Terminal,
Trash2,
Ban,
StopCircle,
} from "lucide-react";
import { commandsApi, Command } from "../api/client";
import { Card, CardContent } from "../components/Card";
import { Button } from "../components/Button";
@@ -28,6 +39,11 @@ function StatusBadge({ status }: { status: Command["status"] }) {
label: "Failed",
className: "bg-rose-500/10 text-rose-400 border-rose-500/30",
},
cancelled: {
icon: Ban,
label: "Cancelled",
className: "bg-amber-500/10 text-amber-500 border-amber-500/30",
},
};
const { icon: Icon, label, className, spin } = config[status] as {
@@ -62,12 +78,63 @@ function formatRelativeTime(dateString: string): string {
}
export function History() {
const queryClient = useQueryClient();
const { data: commands = [], isLoading, refetch } = useQuery({
queryKey: ["commands"],
queryFn: () => commandsApi.list().then((res) => res.data),
refetchInterval: 10000,
});
const cancelMutation = useMutation({
mutationFn: (id: string) => commandsApi.cancelCommand(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to cancel command: ${error.message}`);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => commandsApi.deleteCommand(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to delete command: ${error.message}`);
},
});
const clearHistoryMutation = useMutation({
mutationFn: () => commandsApi.clearCommandHistory(),
onSuccess: (res) => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
const data = res.data;
if (data.deleted === 0) {
alert("No finished commands to clear.");
}
},
onError: (error: Error) => {
alert(`Failed to clear history: ${error.message}`);
},
});
const handleClearHistory = () => {
const finishedCount = commands.filter(
(cmd) => cmd.status === "completed" || cmd.status === "failed" || cmd.status === "cancelled"
).length;
if (finishedCount === 0) {
alert("No finished commands to clear.");
return;
}
if (window.confirm(`Clear ${finishedCount} finished command(s) from history?`)) {
clearHistoryMutation.mutate();
}
};
return (
<div className="space-y-4">
{/* Header */}
@@ -76,15 +143,30 @@ export function History() {
<h1 className="text-2xl font-mono font-bold text-[var(--text-primary)]">History</h1>
<p className="text-[var(--text-muted)] text-sm">Command execution log</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="border-[var(--border-accent)] text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan-muted)]"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<div className="flex items-center gap-2">
<Button
variant="destructive"
size="sm"
onClick={handleClearHistory}
disabled={clearHistoryMutation.isPending}
>
{clearHistoryMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
Clear History
</Button>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="border-[var(--border-accent)] text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan-muted)]"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
{/* History List */}
@@ -103,12 +185,14 @@ export function History() {
) : (
<div className="divide-y divide-[var(--border-secondary)]">
{commands.map((cmd: Command) => (
<Link
<div
key={cmd.id}
to={`/history/${cmd.id}`}
className="flex items-center justify-between p-3 hover:bg-[rgba(6,182,212,0.05)] transition-colors group"
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<Link
to={`/history/${cmd.id}`}
className="flex items-center gap-3 min-w-0 flex-1"
>
<StatusBadge status={cmd.status} />
<div className="min-w-0 flex-1">
<p className="font-mono text-sm text-[var(--text-primary)] truncate group-hover:text-[var(--accent-cyan)] transition-colors">
@@ -118,18 +202,49 @@ export function History() {
{cmd.command_type} | Agent: {cmd.agent_id.slice(0, 8)}...
</p>
</div>
</div>
<div className="text-right pl-4 shrink-0">
<p className="font-mono text-xs text-[var(--text-muted)]">
{formatRelativeTime(cmd.created_at)}
</p>
{cmd.exit_code !== null && (
<p className={`text-xs font-mono ${cmd.exit_code === 0 ? "text-emerald-500" : "text-rose-500"}`}>
exit: {cmd.exit_code}
</Link>
<div className="flex items-center gap-2 pl-4 shrink-0">
<div className="text-right">
<p className="font-mono text-xs text-[var(--text-muted)]">
{formatRelativeTime(cmd.created_at)}
</p>
)}
{cmd.exit_code !== null && (
<p className={`text-xs font-mono ${cmd.exit_code === 0 ? "text-emerald-500" : "text-rose-500"}`}>
exit: {cmd.exit_code}
</p>
)}
</div>
{/* Action buttons */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{(cmd.status === "pending" || cmd.status === "running") && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
cancelMutation.mutate(cmd.id);
}}
disabled={cancelMutation.isPending}
className="p-1.5 rounded-md text-amber-400 hover:bg-amber-500/10 hover:text-amber-300 transition-colors"
title="Cancel command"
>
<StopCircle className="h-4 w-4" />
</button>
)}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
deleteMutation.mutate(cmd.id);
}}
disabled={deleteMutation.isPending}
className="p-1.5 rounded-md text-rose-400 hover:bg-rose-500/10 hover:text-rose-300 transition-colors"
title="Delete command"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
</Link>
</div>
))}
</div>
)}
@@ -142,6 +257,7 @@ export function History() {
export function HistoryDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: commands = [], isLoading } = useQuery({
queryKey: ["commands"],
@@ -150,6 +266,27 @@ export function HistoryDetail() {
const command = commands.find((cmd: Command) => cmd.id === id);
const cancelMutation = useMutation({
mutationFn: (cmdId: string) => commandsApi.cancelCommand(cmdId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to cancel command: ${error.message}`);
},
});
const deleteMutation = useMutation({
mutationFn: (cmdId: string) => commandsApi.deleteCommand(cmdId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
navigate("/history");
},
onError: (error: Error) => {
alert(`Failed to delete command: ${error.message}`);
},
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
@@ -199,6 +336,33 @@ export function HistoryDetail() {
{formatDate(command.created_at)}
</p>
</div>
<div className="flex items-center gap-2">
{(command.status === "pending" || command.status === "running") && (
<Button
variant="outline"
size="sm"
onClick={() => cancelMutation.mutate(command.id)}
disabled={cancelMutation.isPending}
className="border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
>
<StopCircle className="h-4 w-4 mr-2" />
Cancel
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={() => {
if (window.confirm("Delete this command?")) {
deleteMutation.mutate(command.id);
}
}}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Command Info */}