UI: task sessions in agent detail, ApprovalCard extraction, and company settings page

Show task sessions list in AgentDetail with per-session reset. Extract
ApprovalCard into standalone component from Approvals and Inbox pages,
reducing duplication. Add CompanySettings page with issuePrefix configuration.
Fix Sidebar active state for settings route. Display sessionDisplayId
in agent properties. Various cleanups to Approvals and Inbox pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 14:02:29 -06:00
parent 2acf28a51a
commit ea60e4800f
13 changed files with 315 additions and 198 deletions

View File

@@ -48,7 +48,7 @@ import {
ChevronRight,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import type { Agent, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState } from "@paperclip/shared";
import type { Agent, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState, AgentTaskSession } from "@paperclip/shared";
const runStatusIcons: Record<string, { icon: typeof CheckCircle2; color: string }> = {
succeeded: { icon: CheckCircle2, color: "text-green-400" },
@@ -182,6 +182,12 @@ export function AgentDetail() {
enabled: !!agentId,
});
const { data: taskSessions } = useQuery({
queryKey: queryKeys.agents.taskSessions(agentId!),
queryFn: () => agentsApi.taskSessions(agentId!),
enabled: !!agentId,
});
const { data: heartbeats } = useQuery({
queryKey: queryKeys.heartbeats(selectedCompanyId!, agentId),
queryFn: () => heartbeatsApi.list(selectedCompanyId!, agentId),
@@ -205,20 +211,20 @@ export function AgentDetail() {
const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agentId && a.status !== "terminated");
const agentAction = useMutation({
mutationFn: async (action: "invoke" | "pause" | "resume" | "terminate" | "resetSession") => {
mutationFn: async (action: "invoke" | "pause" | "resume" | "terminate") => {
if (!agentId) return Promise.reject(new Error("No agent ID"));
switch (action) {
case "invoke": return agentsApi.invoke(agentId);
case "pause": return agentsApi.pause(agentId);
case "resume": return agentsApi.resume(agentId);
case "terminate": return agentsApi.terminate(agentId);
case "resetSession": return agentsApi.resetSession(agentId);
}
},
onSuccess: () => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) });
queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentId!) });
queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentId!) });
if (selectedCompanyId) {
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) });
}
@@ -228,6 +234,18 @@ export function AgentDetail() {
},
});
const resetTaskSession = useMutation({
mutationFn: (taskKey: string | null) => agentsApi.resetSession(agentId!, taskKey),
onSuccess: () => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentId!) });
queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentId!) });
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Failed to reset session");
},
});
const updatePermissions = useMutation({
mutationFn: (canCreateAgents: boolean) =>
agentsApi.updatePermissions(agentId!, { canCreateAgents }),
@@ -356,12 +374,12 @@ export function AgentDetail() {
<button
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
onClick={() => {
agentAction.mutate("resetSession");
resetTaskSession.mutate(null);
setMoreOpen(false);
}}
>
<RotateCcw className="h-3 w-3" />
Reset Session
Reset Sessions
</button>
<button
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-destructive"
@@ -458,11 +476,14 @@ export function AgentDetail() {
}
</SummaryRow>
<SummaryRow label="Session">
{runtimeState?.sessionId
? <span className="font-mono text-xs">{runtimeState.sessionId.slice(0, 16)}...</span>
{(runtimeState?.sessionDisplayId ?? runtimeState?.sessionId)
? <span className="font-mono text-xs">{String(runtimeState?.sessionDisplayId ?? runtimeState?.sessionId).slice(0, 16)}...</span>
: <span className="text-muted-foreground">No session</span>
}
</SummaryRow>
<SummaryRow label="Task sessions">
<span>{taskSessions?.length ?? 0}</span>
</SummaryRow>
{runtimeState && (
<SummaryRow label="Total spend">
<span>{formatCents(runtimeState.totalCostCents)}</span>
@@ -541,6 +562,13 @@ export function AgentDetail() {
</div>
</div>
</div>
<TaskSessionsCard
sessions={taskSessions ?? []}
onResetTask={(taskKey) => resetTaskSession.mutate(taskKey)}
onResetAll={() => resetTaskSession.mutate(null)}
resetting={resetTaskSession.isPending}
/>
</TabsContent>
{/* CONFIGURATION TAB */}
@@ -603,6 +631,66 @@ function SummaryRow({ label, children }: { label: string; children: React.ReactN
);
}
function TaskSessionsCard({
sessions,
onResetTask,
onResetAll,
resetting,
}: {
sessions: AgentTaskSession[];
onResetTask: (taskKey: string) => void;
onResetAll: () => void;
resetting: boolean;
}) {
return (
<div className="border border-border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Task Sessions</h3>
<Button
variant="outline"
size="sm"
className="h-7 px-2.5 text-xs"
onClick={onResetAll}
disabled={resetting || sessions.length === 0}
>
Reset all
</Button>
</div>
{sessions.length === 0 ? (
<p className="text-sm text-muted-foreground">No task-scoped sessions.</p>
) : (
<div className="space-y-2">
{sessions.slice(0, 20).map((session) => (
<div
key={session.id}
className="flex items-center justify-between border border-border/70 rounded-md px-3 py-2 gap-3"
>
<div className="min-w-0">
<div className="font-mono text-xs truncate">{session.taskKey}</div>
<div className="text-xs text-muted-foreground truncate">
{session.sessionDisplayId
? `session: ${session.sessionDisplayId}`
: "session: <none>"}
{session.lastError ? ` | error: ${session.lastError}` : ""}
</div>
</div>
<Button
variant="outline"
size="sm"
className="h-7 px-2.5 text-xs shrink-0"
onClick={() => onResetTask(session.taskKey)}
disabled={resetting}
>
Reset
</Button>
</div>
))}
</div>
)}
</div>
);
}
/* ---- Configuration Tab ---- */
function ConfigurationTab({