diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index bdf08dea..26434b57 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 { InboxPage } from "../pages/InboxPage"; import { RunsPage } from "../pages/RunsPage"; import { AutomationPage } from "../pages/AutomationPage"; import { MeetingsPage } from "../pages/MeetingsPage"; @@ -45,6 +46,7 @@ export function App() { files: , docs: , knowledge: , + inbox: , runs: , automation: , meetings: , diff --git a/AgentCompany-New/src/renderer/src/pages/InboxPage.tsx b/AgentCompany-New/src/renderer/src/pages/InboxPage.tsx new file mode 100644 index 00000000..edeb33c1 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/InboxPage.tsx @@ -0,0 +1,199 @@ +import { useMemo, useState } from "react"; +import { Avatar, Button, Icon, Pill, Segmented } from "../shared/ui"; +import { TopbarActions } from "../shared/TopbarActions"; +import type { AgentRecord, ApprovalRecord, ChatMessageRecord, TaskRecord, TaskRunRecord } from "../../../shared/domain"; +import { TASK_STATUS } from "../../../shared/domain"; +import type { PageProps } from "./types"; +import { activeOrgId, formatRelative, requireActiveEmployee } from "./types"; + +type InboxFilter = "all" | "approvals" | "failures" | "blocked" | "messages"; +const FILTERS: Array<{ id: InboxFilter; label: string }> = [ + { id: "all", label: "All" }, + { id: "approvals", label: "Approvals" }, + { id: "failures", label: "Failures" }, + { id: "blocked", label: "Blocked" }, + { id: "messages", label: "Messages" }, +]; + +type View = "inbox" | "standup" | "performance"; +const VIEWS: Array<{ id: View; label: string }> = [ + { id: "inbox", label: "Inbox" }, + { id: "standup", label: "Standup" }, + { id: "performance", label: "Performance" }, +]; + +interface InboxItem { + id: string; + kind: InboxFilter; + title: string; + detail: string; + agentName: string | null; + timestamp: string; + tone: "amber" | "red" | "violet" | undefined; +} + +export function InboxPage({ snapshot, actions, navigate }: PageProps) { + const orgId = activeOrgId(snapshot); + const employee = requireActiveEmployee(snapshot); + const agents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]); + const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]); + const taskRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]); + const approvals = useMemo(() => snapshot.approvals.filter((a) => a.orgId === orgId), [snapshot.approvals, orgId]); + const messages = useMemo(() => snapshot.chatMessages.filter((m) => m.orgId === orgId), [snapshot.chatMessages, orgId]); + const [filter, setFilter] = useState("all"); + const [view, setView] = useState("inbox"); + + const items = useMemo(() => buildInboxItems(approvals, taskRuns, tasks, messages, agents), [approvals, taskRuns, tasks, messages, agents]); + const filtered = filter === "all" ? items : items.filter((item) => item.kind === filter); + + return ( +
+ + + +

Inbox

+

{items.length} {items.length === 1 ? "item" : "items"}

+ + {view === "inbox" && ( + <> +
+
+ {filtered.map((item) => ( +
+
+ {item.agentName && } +
+
{item.title}
+
{item.detail}
+
+ {humanize(item.kind)} + {formatRelative(item.timestamp)} +
+
+ ))} +
+ + )} + + {view === "standup" && } + {view === "performance" && } +
+ ); +} + +function StandupView({ agents, tasks, taskRuns }: { agents: AgentRecord[]; tasks: TaskRecord[]; taskRuns: TaskRunRecord[] }) { + return ( +
+ {agents.map((agent) => { + const agentTasks = tasks.filter((t) => t.assigneeAgentId === agent.id); + const inProgress = agentTasks.filter((t) => t.status === TASK_STATUS.IN_PROGRESS).length; + const done = agentTasks.filter((t) => t.status === TASK_STATUS.DONE).length; + const agentRuns = taskRuns.filter((r) => r.agentId === agent.id); + const lastRun = agentRuns[agentRuns.length - 1]; + return ( +
+
+ + {agent.name} + {agent.role} ยท {agent.department} +
+
+ {inProgress} in progress + {done} done + {agentRuns.length} runs + {lastRun && {formatRelative(lastRun.createdAt)}} +
+
+ ); + })} +
+ ); +} + +function PerformanceView({ agents, tasks, taskRuns }: { agents: AgentRecord[]; tasks: TaskRecord[]; taskRuns: TaskRunRecord[] }) { + const totalTasks = tasks.length; + const doneTasks = tasks.filter((t) => t.status === TASK_STATUS.DONE).length; + const completedRuns = taskRuns.filter((r) => r.state === "completed").length; + const failedRuns = taskRuns.filter((r) => r.state === "failed").length; + const successRate = completedRuns + failedRuns > 0 ? Math.round((completedRuns / (completedRuns + failedRuns)) * 100) : 0; + const throughput = doneTasks; + + return ( +
+
+ + + + a.lifecycle === "active").length)} /> +
+
Agent throughput
+
+
+
Agent
Tasks done
Runs
Failures
+
+ {agents.map((agent, i) => { + const agentDone = tasks.filter((t) => t.assigneeAgentId === agent.id && t.status === TASK_STATUS.DONE).length; + const agentRuns = taskRuns.filter((r) => r.agentId === agent.id); + const agentFailed = agentRuns.filter((r) => r.state === "failed").length; + return ( +
+
+ + {agent.name} +
+
{agentDone}
+
{agentRuns.length}
+
{agentFailed > 0 ? {agentFailed} : 0}
+
+ ); + })} +
+
+ ); +} + +function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) { + return ( +
+
{label}
+
+ {value} + {sub && {sub}} +
+
+ ); +} + +function buildInboxItems( + approvals: ApprovalRecord[], + taskRuns: TaskRunRecord[], + tasks: TaskRecord[], + messages: ChatMessageRecord[], + agents: AgentRecord[], +): InboxItem[] { + const items: InboxItem[] = []; + for (const a of approvals.filter((a) => a.state === "pending")) { + items.push({ id: `app-${a.id}`, kind: "approvals", title: a.title, detail: a.kind.replace(/_/g, " "), agentName: null, timestamp: a.createdAt, tone: "amber" }); + } + for (const r of taskRuns.filter((r) => r.state === "failed")) { + const task = tasks.find((t) => t.id === r.taskId); + const agent = agents.find((a) => a.id === r.agentId); + items.push({ id: `fail-${r.id}`, kind: "failures", title: task?.title ?? r.taskId, detail: r.errorMessage ?? "Run failed", agentName: agent?.name ?? null, timestamp: r.completedAt ?? r.createdAt, tone: "red" }); + } + for (const t of tasks.filter((t) => t.status === TASK_STATUS.REVIEW)) { + const agent = agents.find((a) => a.id === t.assigneeAgentId); + items.push({ id: `blk-${t.id}`, kind: "blocked", title: t.title, detail: "Awaiting review", agentName: agent?.name ?? null, timestamp: t.updatedAt, tone: "violet" }); + } + const recent = messages.filter((m) => m.senderType === "agent").slice(-10); + for (const m of recent) { + const agent = agents.find((a) => a.id === m.senderId); + const preview = m.bodyMd.length > 60 ? `${m.bodyMd.slice(0, 57)}...` : m.bodyMd; + items.push({ id: `msg-${m.id}`, kind: "messages", title: agent?.name ?? "Agent", detail: preview, agentName: agent?.name ?? null, timestamp: m.createdAt, tone: undefined }); + } + items.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); + return items; +} + +function humanize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} diff --git a/AgentCompany-New/src/renderer/src/shared/Shell.tsx b/AgentCompany-New/src/renderer/src/shared/Shell.tsx index d13a2fbe..fd94d674 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" + | "inbox" | "runs" | "meetings" | "sprints" @@ -38,6 +39,7 @@ const primaryNav: NavSpec[] = [ { id: "work", label: "Work", icon: "work" }, { id: "chat", label: "Messages", icon: "chat" }, { id: "files", label: "Files", icon: "docs" }, + { id: "inbox", label: "Inbox", icon: "approve" }, { id: "docs", label: "Documents", icon: "file" }, { id: "knowledge", label: "Knowledge", icon: "sparkle" }, { id: "runs", label: "Runs", icon: "send" },