Add agent run activity and recent messages to homepage
The homepage now shows two activity cards: recent agent runs (with state, trigger, and timing) and recent chat messages (with sender, channel, and message preview). Gives the operator real-time visibility into what agents are doing.
This commit is contained in:
1 parent
8f4ccc1b7a
commit
2ef721774f
1 file changed
+123
-1
@@ -4,14 +4,16 @@ import { TopbarActions } from "../shared/TopbarActions";
|
||||
import type {
|
||||
AgentRecord,
|
||||
ApprovalRecord,
|
||||
ChatMessageRecord,
|
||||
EmployeeRecord,
|
||||
GoalRecord,
|
||||
ProviderRecord,
|
||||
TaskRecord,
|
||||
TaskRunRecord,
|
||||
} from "../../../shared/domain";
|
||||
import { NewTaskDialog } from "./NewTaskDialog";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, formatTime, money, requireActiveEmployee, requireActiveOrganization } from "./types";
|
||||
import { activeOrgId, formatRelative, formatTime, money, requireActiveEmployee, requireActiveOrganization } from "./types";
|
||||
|
||||
export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
const orgId = activeOrgId(snapshot);
|
||||
@@ -32,6 +34,9 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
.reduce((sum, policy) => sum + policy.monthlyBudgetLimitCents, 0),
|
||||
[snapshot.hiringPolicies, orgId],
|
||||
);
|
||||
const taskRuns = useMemo(() => snapshot.taskRuns.filter((run) => run.orgId === orgId), [snapshot.taskRuns, orgId]);
|
||||
const chatMessages = useMemo(() => snapshot.chatMessages.filter((m) => m.orgId === orgId), [snapshot.chatMessages, orgId]);
|
||||
const chatSpaces = useMemo(() => snapshot.chatSpaces.filter((s) => s.orgId === orgId), [snapshot.chatSpaces, orgId]);
|
||||
const [newTaskOpen, setNewTaskOpen] = useState(false);
|
||||
|
||||
const pendingApprovals = approvals.filter((approval) => approval.state === "pending");
|
||||
@@ -77,6 +82,11 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
<GoalCard goal={primaryGoal} progress={goalProgress} organizationName={organization.name} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)", gap: 12, marginTop: 12 }}>
|
||||
<RunActivityCard taskRuns={taskRuns} tasks={tasks} agents={agents} onViewAll={() => navigate("work")} />
|
||||
<RecentMessagesCard messages={chatMessages} agents={agents} employees={[employee]} spaces={chatSpaces} onViewAll={() => navigate("chat")} />
|
||||
</div>
|
||||
|
||||
<NewTaskDialog open={newTaskOpen} orgId={orgId} snapshot={snapshot} actions={actions} onClose={() => setNewTaskOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
@@ -366,3 +376,115 @@ 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 (
|
||||
<section className="card" style={{ padding: 18 }}>
|
||||
<div className="row" style={{ marginBottom: 14 }}>
|
||||
<span className="card-label">Agent runs</span>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: 11 }} onClick={onViewAll}>
|
||||
All work →
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{recent.map((run) => {
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
return (
|
||||
<div key={run.id} className="row" style={{ gap: 10 }}>
|
||||
{agent && <Avatar name={agent.name} size="sm" bot />}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{task?.title ?? run.taskId}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>
|
||||
{agent?.name ?? "Agent"} · {run.trigger}
|
||||
</div>
|
||||
</div>
|
||||
<Pill tone={runStateTone(run.state)}>{runStateLabel(run.state)}</Pill>
|
||||
<span className="mono" style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(run.createdAt)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{recent.length === 0 && <div style={{ fontSize: 13, color: "var(--text-3)" }}>No runs yet</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="card" style={{ padding: 18 }}>
|
||||
<div className="row" style={{ marginBottom: 14 }}>
|
||||
<span className="card-label">Recent messages</span>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: 11 }} onClick={onViewAll}>
|
||||
Open chat →
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{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 (
|
||||
<div key={msg.id} className="row" style={{ gap: 10, alignItems: "flex-start" }}>
|
||||
<Avatar name={name} size="sm" bot={msg.senderType === "agent"} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<span style={{ fontSize: 12.5, fontWeight: 540 }}>{name}</span>
|
||||
{space && <span style={{ fontSize: 11, color: "var(--text-3)" }}>#{space.name}</span>}
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>{formatTime(msg.createdAt)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.4 }}>
|
||||
{preview}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{recent.length === 0 && <div style={{ fontSize: 13, color: "var(--text-3)" }}>No messages yet</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function runStateTone(state: string): "red" | "amber" | "violet" | undefined {
|
||||
if (state === "failed") return "red";
|
||||
if (state === "running") return "violet";
|
||||
if (state === "queued") return "amber";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function runStateLabel(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;
|
||||
}
|
||||
Reference in new issue
Block a user