diff --git a/AgentCompany-New/src/renderer/src/pages/HomePage.tsx b/AgentCompany-New/src/renderer/src/pages/HomePage.tsx
index 23a3f083..c06ef15a 100644
--- a/AgentCompany-New/src/renderer/src/pages/HomePage.tsx
+++ b/AgentCompany-New/src/renderer/src/pages/HomePage.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from "react";
+import { useMemo, useState, type CSSProperties } from "react";
import { Avatar, Button, Icon, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import type {
@@ -12,10 +12,14 @@ import type {
TaskRunRecord,
} from "../../../shared/domain";
import { NewTaskDialog } from "./NewTaskDialog";
-import { AgentWorkloadChart, BudgetUtilizationChart, CostTrendChart, RunActivityChart, SuccessRateChart } from "./home/DashboardCharts";
+import { AgentWorkloadChart, CostTrendChart, RunActivityChart, SuccessRateChart } from "./home/DashboardCharts";
import type { PageProps } from "./types";
import { activeOrgId, formatRelative, formatTime, money, requireActiveEmployee, requireActiveOrganization } from "./types";
+const STRIP: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 16, marginBottom: 16 };
+const METRICS: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 16, marginBottom: 16 };
+const ACTIVITY: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 16 };
+
export function HomePage({ snapshot, actions, navigate }: PageProps) {
const orgId = activeOrgId(snapshot);
const organization = requireActiveOrganization(snapshot);
@@ -24,15 +28,9 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
const agents = useMemo(() => snapshot.agents.filter((agent) => agent.orgId === orgId), [snapshot.agents, orgId]);
const approvals = useMemo(() => snapshot.approvals.filter((item) => item.orgId === orgId), [snapshot.approvals, orgId]);
const goals = useMemo(() => snapshot.goals.filter((item) => item.orgId === orgId), [snapshot.goals, orgId]);
- const providers = useMemo(
- () => snapshot.providers.filter((provider) => provider.orgId === orgId),
- [snapshot.providers, orgId],
- );
+ const providers = useMemo(() => snapshot.providers.filter((provider) => provider.orgId === orgId), [snapshot.providers, orgId]);
const budgetCapCents = useMemo(
- () =>
- snapshot.hiringPolicies
- .filter((policy) => policy.orgId === orgId)
- .reduce((sum, policy) => sum + policy.monthlyBudgetLimitCents, 0),
+ () => snapshot.hiringPolicies.filter((policy) => policy.orgId === orgId).reduce((sum, policy) => sum + policy.monthlyBudgetLimitCents, 0),
[snapshot.hiringPolicies, orgId],
);
const taskRuns = useMemo(() => snapshot.taskRuns.filter((run) => run.orgId === orgId), [snapshot.taskRuns, orgId]);
@@ -42,57 +40,41 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
const [newTaskOpen, setNewTaskOpen] = useState(false);
const pendingApprovals = approvals.filter((approval) => approval.state === "pending");
- const inProgressTasks = tasks.filter((task) => task.status === "in_progress");
- const today = new Date();
- const agendaTasks = collectAgendaTasks(tasks, today);
const totalBudgetCents = agents.reduce((sum, agent) => sum + agent.budgetCents, 0);
const primaryGoal = goals.find((goal) => goal.status === "active") ?? goals[0] ?? null;
const goalProgress = computeGoalProgress(primaryGoal, tasks);
const firstName = employee.displayName.split(/\s+/)[0] ?? employee.displayName;
- const greeting = greetingFor(today.getHours());
- const summaryText = renderSummary(pendingApprovals.length, agents.length);
+ const greeting = greetingFor(new Date().getHours());
return (
-
+
-
-
+
+
{greeting}, {firstName}
-
{summaryText}
+
{renderSummary(pendingApprovals.length, agents.length)}
-
-
+
+
+
void actions.run(() => window.agentCompany.decideApproval({
- orgId,
- approvalId,
- state: "approved",
- decidedByEmployeeId: employee.id,
- }))}
+ onApprove={(approvalId) => void actions.run(() => window.agentCompany.decideApproval({ orgId, approvalId, state: "approved", decidedByEmployeeId: employee.id }))}
onOpen={() => navigate("approvals")}
/>
-
-
- navigate("work")} />
-
-
-
-
-
+
-
+
navigate("runs")} />
navigate("chat")} />
@@ -102,86 +84,28 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
);
}
-function TodayCard({
- date,
- tasks,
- agents,
- employee,
- approvals,
+function GoalCard({
+ goal,
+ progress,
+ organizationName,
}: {
- date: Date;
- tasks: TaskRecord[];
- agents: AgentRecord[];
- employee: EmployeeRecord;
- approvals: ApprovalRecord[];
+ goal: GoalRecord | null;
+ progress: { percent: number; status: "ahead" | "on_track" | "behind" } | null;
+ organizationName: string;
}) {
- const formatted = date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
- const rows = tasks.slice(0, 4);
+ const percent = progress?.percent ?? 0;
return (
-
-
-
Today
-
{formatted}
+
+ Quarter goal
+
+ {goal ? goal.title : `Set the first goal for ${organizationName}.`}
-
- {rows.map((task) => {
- const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
- const ownerName = owner?.name ?? employee.displayName;
- const time = agendaTime(task, date);
- const requiresApproval = approvals.some(
- (approval) => approval.sourceEntityType === "task" && approval.sourceEntityId === task.id,
- );
- return (
-
-
{time}
-
- {task.title}
- · {ownerName}
-
- {requiresApproval &&
You}
-
- );
- })}
+
+ {progress ? `${percent}%` : "—"}
+ {progress ? statusLabel(progress.status) : "no work yet"}
-
- );
-}
-
-function WaitingCard({
- approvals,
- agents,
- employees,
- onApprove,
- onOpen,
-}: {
- approvals: ApprovalRecord[];
- agents: AgentRecord[];
- employees: EmployeeRecord[];
- onApprove: (approvalId: string) => void;
- onOpen: () => void;
-}) {
- return (
-
-
-
Waiting on you
- {approvals.length > 0 &&
{approvals.length}}
-
-
- {approvals.slice(0, 2).map((approval) => {
- const from = resolveRequester(approval, employees, agents);
- return (
-
-
{approval.title}
-
- {humanizeLabel(approval.kind)} · {humanizeLabel(approval.risk)} risk · from {from}
-
-
-
-
-
-
- );
- })}
+
);
@@ -201,143 +125,159 @@ function SpendCard({
const connected = providers.filter((provider) => provider.enabled);
const fillPct = capCents > 0 ? Math.min(100, Math.round((totalBudgetCents / capCents) * 100)) : 0;
return (
-
- Monthly budget
+
+ Monthly budget
- {money(totalBudgetCents)}
+ {money(totalBudgetCents)}
{capCents > 0 ? `/ ${money(capCents)}` : "allocated"}
-
+
-
+
{connected.slice(0, 3).map((provider) => {
- const allocatedCents = agents
- .filter((agent) => agent.modelProviderId === provider.id)
- .reduce((sum, agent) => sum + agent.budgetCents, 0);
+ const allocatedCents = agents.filter((agent) => agent.modelProviderId === provider.id).reduce((sum, agent) => sum + agent.budgetCents, 0);
return (
-
-
{provider.label}
-
- {money(allocatedCents)}
-
+
+ {provider.label}
+ {money(allocatedCents)}
);
})}
+ {connected.length === 0 &&
No providers connected}
);
}
-function InProgressCard({ tasks, agents, onViewAll }: { tasks: TaskRecord[]; agents: AgentRecord[]; onViewAll: () => void }) {
- const rows = tasks.slice(0, 4);
- return (
-
-
- In progress
-
-
- {rows.map((task, index) => {
- const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
- const last = index === rows.length - 1;
- return (
-
-
-
{task.title}
- {owner &&
{owner.department}}
- {owner &&
}
-
- );
- })}
-
- );
-}
-
-function TeamCard({ agents, employee }: { agents: AgentRecord[]; employee: EmployeeRecord }) {
- return (
-
- Team
- {agents.slice(0, 4).map((agent) => (
-
-
-
{agent.name}
-
{agent.role}
-
- ))}
-
-
-
{employee.displayName}
-
{employee.role} · human
-
-
- );
-}
-
-function GoalCard({
- goal,
- progress,
- organizationName,
+function WaitingCard({
+ approvals,
+ agents,
+ employees,
+ onApprove,
+ onOpen,
}: {
- goal: GoalRecord | null;
- progress: { percent: number; status: "ahead" | "on_track" | "behind" } | null;
- organizationName: string;
+ approvals: ApprovalRecord[];
+ agents: AgentRecord[];
+ employees: EmployeeRecord[];
+ onApprove: (approvalId: string) => void;
+ onOpen: () => void;
}) {
return (
-
-
- Quarter goal
+
+
+
Waiting on you
+ {approvals.length > 0 &&
{approvals.length}}
-
- {goal ? goal.title : `Set the first goal for ${organizationName}.`}
+ {approvals.length === 0 ? (
+
+ You're all caught up
+
+ ) : (
+
+ {approvals.slice(0, 2).map((approval) => (
+
+
{approval.title}
+
+ {humanizeLabel(approval.kind)} · {humanizeLabel(approval.risk)} risk · from {resolveRequester(approval, employees, agents)}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+function RunActivityCard({
+ taskRuns,
+ tasks,
+ agents,
+ onViewAll,
+}: {
+ taskRuns: TaskRunRecord[];
+ tasks: TaskRecord[];
+ agents: AgentRecord[];
+ onViewAll: () => void;
+}) {
+ const recent = taskRuns.slice(0, 6);
+ return (
+
+
+ Agent runs
+
-
- {progress ? `${progress.percent}%` : "—"}
-
- {progress ? statusLabel(progress.status) : "no work yet"}
-
-
-
-
+
+ {recent.map((run) => {
+ const task = tasks.find((t) => t.id === run.taskId);
+ const agent = agents.find((a) => a.id === run.agentId);
+ return (
+
+ {agent &&
}
+
+
{task?.title ?? run.taskId}
+
{agent?.name ?? "Agent"} · {run.trigger}
+
+
{runStateLabel(run.state)}
+
{formatRelative(run.createdAt)}
+
+ );
+ })}
+ {recent.length === 0 &&
No runs yet
}
);
}
-function agendaTime(task: TaskRecord, reference: Date): string {
- const updated = new Date(task.updatedAt);
- if (Number.isNaN(updated.getTime())) return "—";
- const sameDay =
- updated.getFullYear() === reference.getFullYear() &&
- updated.getMonth() === reference.getMonth() &&
- updated.getDate() === reference.getDate();
- if (!sameDay) return "—";
- return formatTime(task.updatedAt);
-}
-
-function collectAgendaTasks(tasks: TaskRecord[], reference: Date): TaskRecord[] {
- const startOfDay = new Date(reference.getFullYear(), reference.getMonth(), reference.getDate()).getTime();
- const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
- const todays = tasks.filter((task) => {
- const updated = new Date(task.updatedAt).getTime();
- return updated >= startOfDay && updated < endOfDay && task.status !== "done";
- });
- if (todays.length > 0) return todays.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
- return tasks.filter((task) => task.status !== "done").slice(0, 4);
+function RecentMessagesCard({
+ messages,
+ agents,
+ employees,
+ spaces,
+ onViewAll,
+}: {
+ messages: ChatMessageRecord[];
+ agents: AgentRecord[];
+ employees: EmployeeRecord[];
+ spaces: Array<{ id: string; name: string }>;
+ onViewAll: () => void;
+}) {
+ const recent = messages.slice(-6).reverse();
+ return (
+
+
+ Recent messages
+
+
+
+ {recent.map((msg) => {
+ const senderAgent = msg.senderType === "agent" ? agents.find((a) => a.id === msg.senderId) : null;
+ const senderEmployee = msg.senderType === "employee" ? employees.find((e) => e.id === msg.senderId) : null;
+ const name = senderAgent?.name ?? senderEmployee?.displayName ?? msg.senderType;
+ const space = spaces.find((s) => s.id === msg.spaceId);
+ const preview = msg.bodyMd.length > 80 ? `${msg.bodyMd.slice(0, 77)}...` : msg.bodyMd;
+ return (
+
+
+
+
+ {name}
+ {space && #{space.name}}
+ {formatTime(msg.createdAt)}
+
+
{preview}
+
+
+ );
+ })}
+ {recent.length === 0 &&
No messages yet
}
+
+
+ );
}
function computeGoalProgress(goal: GoalRecord | null, tasks: TaskRecord[]): { percent: number; status: "ahead" | "on_track" | "behind" } | null {
@@ -387,107 +327,11 @@ function resolveRequester(approval: ApprovalRecord, employees: EmployeeRecord[],
return "System";
}
-function RunActivityCard({
- taskRuns,
- tasks,
- agents,
- onViewAll,
-}: {
- taskRuns: TaskRunRecord[];
- tasks: TaskRecord[];
- agents: AgentRecord[];
- onViewAll: () => void;
-}) {
- const recent = taskRuns.slice(0, 8);
- return (
-
-
- Agent runs
-
-
-
- {recent.map((run) => {
- const task = tasks.find((t) => t.id === run.taskId);
- const agent = agents.find((a) => a.id === run.agentId);
- return (
-
- {agent &&
}
-
-
- {task?.title ?? run.taskId}
-
-
- {agent?.name ?? "Agent"} · {run.trigger}
-
-
-
{runStateLabel(run.state)}
-
{formatRelative(run.createdAt)}
-
- );
- })}
- {recent.length === 0 &&
No runs yet
}
-
-
- );
-}
-
-function RecentMessagesCard({
- messages,
- agents,
- employees,
- spaces,
- onViewAll,
-}: {
- messages: ChatMessageRecord[];
- agents: AgentRecord[];
- employees: EmployeeRecord[];
- spaces: Array<{ id: string; name: string }>;
- onViewAll: () => void;
-}) {
- const recent = messages.slice(-8).reverse();
- return (
-
-
- Recent messages
-
-
-
- {recent.map((msg) => {
- const senderAgent = msg.senderType === "agent" ? agents.find((a) => a.id === msg.senderId) : null;
- const senderEmployee = msg.senderType === "employee" ? employees.find((e) => e.id === msg.senderId) : null;
- const name = senderAgent?.name ?? senderEmployee?.displayName ?? msg.senderType;
- const space = spaces.find((s) => s.id === msg.spaceId);
- const preview = msg.bodyMd.length > 80 ? `${msg.bodyMd.slice(0, 77)}...` : msg.bodyMd;
- return (
-
-
-
-
- {name}
- {space && #{space.name}}
- {formatTime(msg.createdAt)}
-
-
- {preview}
-
-
-
- );
- })}
- {recent.length === 0 &&
No messages yet
}
-
-
- );
-}
-
-function runStateTone(state: string): "red" | "amber" | "violet" | undefined {
+function runStateTone(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;
}
diff --git a/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx b/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx
index a1c2ef01..0773fff3 100644
--- a/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx
+++ b/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx
@@ -1,27 +1,55 @@
-import type { AgentRecord, ProviderRecord, TaskRecord, TaskRunRecord } from "../../../../shared/domain";
+import { useState, type ReactNode } from "react";
+import type { AgentRecord, TaskRecord, TaskRunRecord } from "../../../../shared/domain";
import { TASK_STATUS } from "../../../../shared/domain";
-const CHART_H = 120;
+const CHART_H = 116;
const CHART_W = 260;
const BAR_GAP = 4;
+/** A metric tile that lifts on hover and surfaces a live readout in its
+ * header — the hovered datum's value, or a resting summary otherwise. */
+function ChartCard({ title, readout, children }: { title: string; readout: string; children: ReactNode }) {
+ return (
+
+
+ {title}
+ {readout}
+
+ {children}
+
+ );
+}
+
export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; tasks: TaskRecord[] }) {
const data = agents.slice(0, 8).map((a) => ({
- name: a.name.split(/\s+/)[0],
+ name: a.name.split(/\s+/)[0] ?? a.name,
count: tasks.filter((t) => t.assigneeAgentId === a.id && t.status !== TASK_STATUS.DONE).length,
}));
+ const total = data.reduce((s, d) => s + d.count, 0);
const max = Math.max(1, ...data.map((d) => d.count));
const barW = data.length > 0 ? (CHART_W - (data.length - 1) * BAR_GAP) / data.length : 0;
+ const [hover, setHover] = useState
(null);
+ const readout = hover !== null && data[hover] ? `${data[hover]!.name} · ${data[hover]!.count}` : `${total} open`;
return (
-
+