UI: URL-based tab routing, ActivityRow extraction, and agent detail redesign
Switch agents, issues, and approvals pages from query-param tabs to URL-based routes (/agents/active, /issues/backlog, /approvals/pending). Extract shared ActivityRow component used by both Dashboard and Activity pages. Redesign agent detail overview with LatestRunCard showing live/ recent run status, move permissions toggle to Configuration tab, add budget progress bar, and reorder tabs (Runs before Configuration). Dashboard now counts idle agents as active and shows "Recent Tasks" instead of "Stale Tasks". Remove unused MyIssues page and sidebar link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { agentsApi } from "../api/agents";
|
||||
@@ -10,8 +9,7 @@ import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { Identity } from "../components/Identity";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { ActivityRow } from "../components/ActivityRow";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,74 +20,9 @@ import {
|
||||
import { History } from "lucide-react";
|
||||
import type { Agent } from "@paperclip/shared";
|
||||
|
||||
// Maps action → verb phrase. When the entity name is available it reads as:
|
||||
// "[Actor] commented on "Fix the bug""
|
||||
// When not available, it falls back to just the verb.
|
||||
const ACTION_VERBS: Record<string, string> = {
|
||||
"issue.created": "created",
|
||||
"issue.updated": "updated",
|
||||
"issue.checked_out": "checked out",
|
||||
"issue.released": "released",
|
||||
"issue.comment_added": "commented on",
|
||||
"issue.commented": "commented on",
|
||||
"issue.deleted": "deleted",
|
||||
"agent.created": "created",
|
||||
"agent.updated": "updated",
|
||||
"agent.paused": "paused",
|
||||
"agent.resumed": "resumed",
|
||||
"agent.terminated": "terminated",
|
||||
"agent.key_created": "created API key for",
|
||||
"agent.budget_updated": "updated budget for",
|
||||
"agent.runtime_session_reset": "reset session for",
|
||||
"heartbeat.invoked": "invoked heartbeat for",
|
||||
"heartbeat.cancelled": "cancelled heartbeat for",
|
||||
"approval.created": "requested approval",
|
||||
"approval.approved": "approved",
|
||||
"approval.rejected": "rejected",
|
||||
"project.created": "created",
|
||||
"project.updated": "updated",
|
||||
"project.deleted": "deleted",
|
||||
"goal.created": "created",
|
||||
"goal.updated": "updated",
|
||||
"goal.deleted": "deleted",
|
||||
"cost.reported": "reported cost for",
|
||||
"cost.recorded": "recorded cost for",
|
||||
"company.created": "created",
|
||||
"company.updated": "updated",
|
||||
"company.archived": "archived",
|
||||
"company.budget_updated": "updated budget for",
|
||||
};
|
||||
|
||||
function entityLink(entityType: string, entityId: string): string | null {
|
||||
switch (entityType) {
|
||||
case "issue":
|
||||
return `/issues/${entityId}`;
|
||||
case "agent":
|
||||
return `/agents/${entityId}`;
|
||||
case "project":
|
||||
return `/projects/${entityId}`;
|
||||
case "goal":
|
||||
return `/goals/${entityId}`;
|
||||
case "approval":
|
||||
return `/approvals/${entityId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function actorIdentity(actorType: string, actorId: string, agentMap: Map<string, Agent>) {
|
||||
if (actorType === "agent") {
|
||||
const agent = agentMap.get(actorId);
|
||||
return <Identity name={agent?.name ?? actorId.slice(0, 8)} size="sm" />;
|
||||
}
|
||||
if (actorType === "system") return <Identity name="System" size="sm" />;
|
||||
return <Identity name={actorId || "You"} size="sm" />;
|
||||
}
|
||||
|
||||
export function Activity() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,7 +65,6 @@ export function Activity() {
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
||||
// Unified map: "entityType:entityId" → display name
|
||||
const entityNameMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const i of issues ?? []) map.set(`issue:${i.id}`, i.title);
|
||||
@@ -182,31 +114,14 @@ export function Activity() {
|
||||
|
||||
{filtered && filtered.length > 0 && (
|
||||
<div className="border border-border divide-y divide-border">
|
||||
{filtered.map((event) => {
|
||||
const link = entityLink(event.entityType, event.entityId);
|
||||
const verb = ACTION_VERBS[event.action] ?? event.action.replace(/[._]/g, " ");
|
||||
const name = entityNameMap.get(`${event.entityType}:${event.entityId}`);
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`px-4 py-2.5 flex items-center justify-between gap-4 ${
|
||||
link ? "cursor-pointer hover:bg-accent/50 transition-colors" : ""
|
||||
}`}
|
||||
onClick={link ? () => navigate(link) : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{actorIdentity(event.actorType, event.actorId, agentMap)}
|
||||
<span className="text-sm text-muted-foreground">{verb}</span>
|
||||
{name && (
|
||||
<span className="text-sm truncate">{name}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{timeAgo(event.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{filtered.map((event) => (
|
||||
<ActivityRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
agentMap={agentMap}
|
||||
entityNameMap={entityNameMap}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user