diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index 794ba220..bdf08dea 100644 --- a/AgentCompany-New/src/renderer/src/app/App.tsx +++ b/AgentCompany-New/src/renderer/src/app/App.tsx @@ -11,6 +11,7 @@ import { ApprovalsPage } from "../pages/ApprovalsPage"; import { SocialPage } from "../pages/SocialPage"; import { DocumentsPage } from "../pages/DocumentsPage"; import { KnowledgePage } from "../pages/KnowledgePage"; +import { RunsPage } from "../pages/RunsPage"; import { AutomationPage } from "../pages/AutomationPage"; import { MeetingsPage } from "../pages/MeetingsPage"; import { SprintsPage } from "../pages/SprintsPage"; @@ -44,6 +45,7 @@ export function App() { files: , docs: , knowledge: , + runs: , automation: , meetings: , sprints: , diff --git a/AgentCompany-New/src/renderer/src/pages/RunsPage.tsx b/AgentCompany-New/src/renderer/src/pages/RunsPage.tsx new file mode 100644 index 00000000..4d78a079 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/RunsPage.tsx @@ -0,0 +1,210 @@ +import { useMemo, useState } from "react"; +import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui"; +import { TopbarActions } from "../shared/TopbarActions"; +import type { AgentRecord, ProviderRecord, TaskRecord, TaskRunRecord } from "../../../shared/domain"; +import type { PageProps } from "./types"; +import { activeOrgId, formatRelative } from "./types"; + +type StatusFilter = "all" | "queued" | "running" | "completed" | "failed"; +const FILTERS: Array<{ id: StatusFilter; label: string }> = [ + { id: "all", label: "All" }, + { id: "queued", label: "Queued" }, + { id: "running", label: "Running" }, + { id: "completed", label: "Done" }, + { id: "failed", label: "Failed" }, +]; + +export function RunsPage({ snapshot, actions }: PageProps) { + const orgId = activeOrgId(snapshot); + const taskRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]); + const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]); + const agents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]); + const providers = useMemo(() => snapshot.providers.filter((p) => p.orgId === orgId), [snapshot.providers, orgId]); + const providerRequests = useMemo(() => snapshot.providerRequests.filter((r) => r.orgId === orgId), [snapshot.providerRequests, orgId]); + const [filter, setFilter] = useState("all"); + const [selectedId, setSelectedId] = useState(null); + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + let list = filter === "all" ? taskRuns : taskRuns.filter((r) => r.state === filter); + if (search.trim()) { + const q = search.toLowerCase(); + list = list.filter((r) => { + const task = tasks.find((t) => t.id === r.taskId); + const agent = agents.find((a) => a.id === r.agentId); + return task?.title.toLowerCase().includes(q) || agent?.name.toLowerCase().includes(q) || r.id.includes(q); + }); + } + return list; + }, [taskRuns, filter, search, tasks, agents]); + + const selected = selectedId ? taskRuns.find((r) => r.id === selectedId) ?? null : null; + const queued = taskRuns.filter((r) => r.state === "queued").length; + const running = taskRuns.filter((r) => r.state === "running").length; + const failed = taskRuns.filter((r) => r.state === "failed").length; + + return ( +
+ +
+ {selected ? ( + + ) : ( +
+ Select a run +
+ )} +
+
+ ); +} + +function RunDetail({ run, tasks, agents, providers, providerRequests }: { + run: TaskRunRecord; + tasks: TaskRecord[]; + agents: AgentRecord[]; + providers: ProviderRecord[]; + providerRequests: Array<{ id: string; orgId: string; providerId: string; model: string; state: string; responseText: string | null; inputCharCount: number; outputCharCount: number; createdAt: string }>; +}) { + const task = tasks.find((t) => t.id === run.taskId); + const agent = agents.find((a) => a.id === run.agentId); + const provider = providers.find((p) => p.id === run.providerId); + const request = run.providerRequestId ? providerRequests.find((r) => r.id === run.providerRequestId) : null; + + const started = run.startedAt ? new Date(run.startedAt) : null; + const completed = run.completedAt ? new Date(run.completedAt) : null; + const durationMs = started && completed ? completed.getTime() - started.getTime() : null; + + return ( +
+
+ {agent && } +
+
{task?.title ?? run.taskId}
+
{run.id}
+
+ {runLabel(run.state)} +
+ +
+ Agent
{agent?.name ?? "—"} ({agent?.role ?? "—"})
+ Provider
{provider?.label ?? "—"} · {provider?.model ?? "—"}
+ Trigger
{run.trigger}
+ Created
{formatRelative(run.createdAt)}
+ {run.startedAt && (<>Started
{formatRelative(run.startedAt)}
)} + {run.completedAt && (<>Completed
{formatRelative(run.completedAt)}
)} + {durationMs !== null && (<>Duration
{formatDuration(durationMs)}
)} + Context
{run.executionContextId}
+
+ + {run.state === "failed" && run.errorMessage && ( +
+
Error
+
{run.errorMessage}
+
+ )} + + {request && ( +
+
Provider request
+
+ Request ID
{request.id}
+ Model
{request.model}
+ State
{request.state}
+ Input chars
{request.inputCharCount.toLocaleString()}
+ Output chars
{request.outputCharCount.toLocaleString()}
+
+ {request.responseText && ( +
+
Response
+
+ {request.responseText} +
+
+ )} +
+ )} +
+ ); +} + +function MetaLabel({ children }: { children: string }) { + return
{children}
; +} + +function runTone(state: string): "red" | "amber" | "violet" | "green" | undefined { + if (state === "failed") return "red"; + if (state === "running") return "violet"; + if (state === "queued") return "amber"; + if (state === "completed") return "green"; + return undefined; +} + +function runLabel(state: string): string { + if (state === "queued") return "Queued"; + if (state === "running") return "Running"; + if (state === "completed") return "Done"; + if (state === "failed") return "Failed"; + return state; +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + const seconds = Math.round(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainder = seconds % 60; + return `${minutes}m ${remainder}s`; +} diff --git a/AgentCompany-New/src/renderer/src/shared/Shell.tsx b/AgentCompany-New/src/renderer/src/shared/Shell.tsx index b338fd0d..d13a2fbe 100644 --- a/AgentCompany-New/src/renderer/src/shared/Shell.tsx +++ b/AgentCompany-New/src/renderer/src/shared/Shell.tsx @@ -15,6 +15,7 @@ export type RouteId = | "docs" | "knowledge" | "automation" + | "runs" | "meetings" | "sprints" | "approvals" @@ -29,7 +30,7 @@ interface NavSpec { /** Routes whose pages own a full-height, multi-pane layout with their own * internal scroll regions, so the content area must not scroll as one block. */ -const FLUSH_ROUTES = new Set(["chat", "settings", "files", "social"]); +const FLUSH_ROUTES = new Set(["chat", "settings", "files", "social", "runs"]); const primaryNav: NavSpec[] = [ { id: "home", label: "Home", icon: "home" }, @@ -39,6 +40,7 @@ const primaryNav: NavSpec[] = [ { id: "files", label: "Files", icon: "docs" }, { id: "docs", label: "Documents", icon: "file" }, { id: "knowledge", label: "Knowledge", icon: "sparkle" }, + { id: "runs", label: "Runs", icon: "send" }, { id: "automation", label: "Automation", icon: "bot" }, { id: "meetings", label: "Meetings", icon: "team" }, { id: "sprints", label: "Sprints", icon: "work" },