219 lines
8.2 KiB
TypeScript
219 lines
8.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|||
|
|
import type { ClaudeSession, Project, ScheduledTask } from "../../../lib/types";
|
||
|
|
import {
|
||
|
|
listClaudeSessions,
|
||
|
|
listScheduledTasks,
|
||
|
|
getSchedulerNotifications,
|
||
|
|
resumeSessionCommand,
|
||
|
|
} from "../../../lib/tauri-commands";
|
||
|
|
import type { useProjectActions } from "../../../hooks/useProjectActions";
|
||
|
|
import type { SaveState } from "../../../hooks/useSaveState";
|
||
|
|
import PermissionModeControl, {
|
||
|
|
permissionModePatch,
|
||
|
|
} from "../PermissionModeControl";
|
||
|
|
import CapabilityTiles from "./CapabilityTiles";
|
||
|
|
import SaveIndicator from "../../ui/SaveIndicator";
|
||
|
|
import Button from "../../ui/Button";
|
||
|
|
import { formatAge } from "./format";
|
||
|
|
import type { ProjectHomeTabId } from "./ProjectHome";
|
||
|
|
|
||
|
|
const BACKEND_LABEL: Record<Project["backend"], string> = {
|
||
|
|
anthropic: "Anthropic",
|
||
|
|
bedrock: "AWS Bedrock",
|
||
|
|
ollama: "Ollama",
|
||
|
|
open_ai_compatible: "OpenAI Compatible",
|
||
|
|
};
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
project: Project;
|
||
|
|
save: (patch: Partial<Project>) => Promise<boolean>;
|
||
|
|
saveState: SaveState;
|
||
|
|
actions: ReturnType<typeof useProjectActions>;
|
||
|
|
onOpenTab: (tab: ProjectHomeTabId) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function OverviewTab({
|
||
|
|
project,
|
||
|
|
save,
|
||
|
|
saveState,
|
||
|
|
actions,
|
||
|
|
onOpenTab,
|
||
|
|
}: Props) {
|
||
|
|
const [sessions, setSessions] = useState<ClaudeSession[]>([]);
|
||
|
|
const [tasks, setTasks] = useState<ScheduledTask[]>([]);
|
||
|
|
const [notificationCount, setNotificationCount] = useState(0);
|
||
|
|
const running = project.status === "running";
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!running) {
|
||
|
|
setSessions([]);
|
||
|
|
setTasks([]);
|
||
|
|
setNotificationCount(0);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let cancelled = false;
|
||
|
|
// All three degrade to empty when the container is unreachable.
|
||
|
|
listClaudeSessions(project.id)
|
||
|
|
.then((s) => !cancelled && setSessions(s.slice(0, 4)))
|
||
|
|
.catch(() => !cancelled && setSessions([]));
|
||
|
|
listScheduledTasks(project.id)
|
||
|
|
.then((t) => !cancelled && setTasks(t))
|
||
|
|
.catch(() => !cancelled && setTasks([]));
|
||
|
|
getSchedulerNotifications(project.id)
|
||
|
|
.then((n) => !cancelled && setNotificationCount(n.length))
|
||
|
|
.catch(() => !cancelled && setNotificationCount(0));
|
||
|
|
return () => {
|
||
|
|
cancelled = true;
|
||
|
|
};
|
||
|
|
}, [project.id, running, project.container_id]);
|
||
|
|
|
||
|
|
const handleResume = async (session: ClaudeSession) => {
|
||
|
|
try {
|
||
|
|
const command = await resumeSessionCommand(project.id, session.id);
|
||
|
|
await actions.openTerminalWithCommand(command, session.name ?? "resume");
|
||
|
|
} catch (e) {
|
||
|
|
console.error("Failed to build the resume command:", e);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-4 space-y-6 max-w-4xl">
|
||
|
|
{/* Permission mode — the hero control */}
|
||
|
|
<section className="p-3 border border-[var(--border-color)] rounded-[var(--radius-panel)] bg-[var(--bg-secondary)]">
|
||
|
|
<div className="flex items-start justify-between gap-4">
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<PermissionModeControl
|
||
|
|
project={project}
|
||
|
|
disabled={!running && project.status !== "stopped" && project.status !== "error"}
|
||
|
|
onChange={(mode) => save(permissionModePatch(mode))}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<SaveIndicator state={saveState} />
|
||
|
|
</div>
|
||
|
|
<div className="mt-3 pt-3 border-t border-[var(--border-color)] flex flex-wrap gap-x-6 gap-y-1 text-xs">
|
||
|
|
<span className="text-[var(--text-secondary)]">
|
||
|
|
Backend{" "}
|
||
|
|
<span className="text-[var(--text-primary)] font-medium">
|
||
|
|
{BACKEND_LABEL[project.backend]}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
<span className="text-[var(--text-secondary)]">
|
||
|
|
Docker access{" "}
|
||
|
|
<span className="text-[var(--text-primary)] font-medium">
|
||
|
|
{project.allow_docker_access ? "ON" : "OFF"}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
<span className="text-[var(--text-secondary)]">
|
||
|
|
Mission Control{" "}
|
||
|
|
<span className="text-[var(--text-primary)] font-medium">
|
||
|
|
{project.mission_control_enabled ? "ON" : "OFF"}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onOpenTab("config")}
|
||
|
|
className="text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
|
||
|
|
>
|
||
|
|
Edit configuration →
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<CapabilityTiles
|
||
|
|
project={project}
|
||
|
|
onManageInTerminal={(command) => actions.openTerminalWithCommand(command)}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="grid gap-6 md:grid-cols-2">
|
||
|
|
{/* Recent sessions */}
|
||
|
|
<section>
|
||
|
|
<div className="flex items-baseline justify-between mb-2">
|
||
|
|
<h2 className="text-[11px] font-semibold uppercase tracking-wide text-[var(--text-secondary)]">
|
||
|
|
Recent sessions
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onOpenTab("sessions")}
|
||
|
|
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
|
||
|
|
>
|
||
|
|
All sessions →
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{!running ? (
|
||
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
||
|
|
Start the container to list saved conversations.
|
||
|
|
</p>
|
||
|
|
) : sessions.length === 0 ? (
|
||
|
|
<p className="text-xs text-[var(--text-secondary)]">No sessions yet.</p>
|
||
|
|
) : (
|
||
|
|
<ul className="space-y-1">
|
||
|
|
{sessions.map((session) => (
|
||
|
|
<li
|
||
|
|
key={session.id}
|
||
|
|
className="flex items-center gap-2 px-2 py-1.5 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-[var(--radius-control)]"
|
||
|
|
>
|
||
|
|
<span className="flex-1 min-w-0 text-xs text-[var(--text-primary)] truncate">
|
||
|
|
{session.name ?? session.summary ?? session.id}
|
||
|
|
</span>
|
||
|
|
<span className="text-xs text-[var(--text-secondary)] flex-shrink-0">
|
||
|
|
{formatAge(session.last_modified) ?? ""}
|
||
|
|
</span>
|
||
|
|
<Button onClick={() => handleResume(session)}>Resume</Button>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Scheduled tasks */}
|
||
|
|
<section>
|
||
|
|
<div className="flex items-baseline justify-between mb-2">
|
||
|
|
<h2 className="text-[11px] font-semibold uppercase tracking-wide text-[var(--text-secondary)]">
|
||
|
|
Scheduled tasks
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onOpenTab("automation")}
|
||
|
|
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
|
||
|
|
>
|
||
|
|
Automation →
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{!running ? (
|
||
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
||
|
|
Start the container to list scheduled tasks.
|
||
|
|
</p>
|
||
|
|
) : tasks.length === 0 ? (
|
||
|
|
<p className="text-xs text-[var(--text-secondary)]">No scheduled tasks.</p>
|
||
|
|
) : (
|
||
|
|
<ul className="space-y-1">
|
||
|
|
{tasks.slice(0, 4).map((task) => (
|
||
|
|
<li
|
||
|
|
key={task.id}
|
||
|
|
className="flex items-center gap-2 px-2 py-1.5 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-[var(--radius-control)]"
|
||
|
|
>
|
||
|
|
<span className="flex-1 min-w-0 text-xs text-[var(--text-primary)] truncate">
|
||
|
|
{task.name}
|
||
|
|
</span>
|
||
|
|
<span className="text-xs text-[var(--text-secondary)] font-mono flex-shrink-0">
|
||
|
|
{task.at ?? task.schedule}
|
||
|
|
</span>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
{notificationCount > 0 && (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onOpenTab("automation")}
|
||
|
|
className="mt-2 inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded-[var(--radius-control)] bg-[var(--accent-muted)] text-[var(--accent)] hover:bg-[var(--bg-tertiary)] transition-colors"
|
||
|
|
>
|
||
|
|
{notificationCount} notification{notificationCount === 1 ? "" : "s"}
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|