feat(ui): active agents panel, sidebar context, and page enhancements

Add live ActiveAgentsPanel with real-time transcript feed, SidebarContext
for responsive sidebar state, agent config form with reasoning effort,
improved inbox with failed run alerts, enriched issue detail with project
picker, and various component refinements across pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 10:32:32 -06:00
parent b327687c92
commit adca44849a
29 changed files with 1461 additions and 146 deletions

View File

@@ -1,3 +1,4 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import type { Issue } from "@paperclip/shared";
import { useQuery } from "@tanstack/react-query";
@@ -8,9 +9,11 @@ import { queryKeys } from "../lib/queryKeys";
import { StatusIcon } from "./StatusIcon";
import { PriorityIcon } from "./PriorityIcon";
import { Identity } from "./Identity";
import { formatDate } from "../lib/utils";
import { formatDate, cn } from "../lib/utils";
import { timeAgo } from "../lib/timeAgo";
import { Separator } from "@/components/ui/separator";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { User, Hexagon, ArrowUpRight } from "lucide-react";
interface IssuePropertiesProps {
issue: Issue;
@@ -26,16 +29,12 @@ function PropertyRow({ label, children }: { label: string; children: React.React
);
}
function statusLabel(status: string): string {
return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
function priorityLabel(priority: string): string {
return priority.charAt(0).toUpperCase() + priority.slice(1);
}
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
const { selectedCompanyId } = useCompany();
const [assigneeOpen, setAssigneeOpen] = useState(false);
const [assigneeSearch, setAssigneeSearch] = useState("");
const [projectOpen, setProjectOpen] = useState(false);
const [projectSearch, setProjectSearch] = useState("");
const { data: agents } = useQuery({
queryKey: queryKeys.agents.list(selectedCompanyId!),
@@ -46,7 +45,7 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
const { data: projects } = useQuery({
queryKey: queryKeys.projects.list(selectedCompanyId!),
queryFn: () => projectsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId && !!issue.projectId,
enabled: !!selectedCompanyId,
});
const agentName = (id: string | null) => {
@@ -72,41 +71,142 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
<StatusIcon
status={issue.status}
onChange={(status) => onUpdate({ status })}
showLabel
/>
<span className="text-sm">{statusLabel(issue.status)}</span>
</PropertyRow>
<PropertyRow label="Priority">
<PriorityIcon
priority={issue.priority}
onChange={(priority) => onUpdate({ priority })}
showLabel
/>
<span className="text-sm">{priorityLabel(issue.priority)}</span>
</PropertyRow>
<PropertyRow label="Assignee">
{assignee ? (
<Popover open={assigneeOpen} onOpenChange={(open) => { setAssigneeOpen(open); if (!open) setAssigneeSearch(""); }}>
<PopoverTrigger asChild>
<button className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1 py-0.5 transition-colors">
{assignee ? (
<Identity name={assignee.name} size="sm" />
) : (
<>
<User className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Unassigned</span>
</>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-52 p-1" align="end">
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder="Search agents..."
value={assigneeSearch}
onChange={(e) => setAssigneeSearch(e.target.value)}
autoFocus
/>
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
!issue.assigneeAgentId && "bg-accent"
)}
onClick={() => { onUpdate({ assigneeAgentId: null }); setAssigneeOpen(false); }}
>
No assignee
</button>
{(agents ?? [])
.filter((a) => a.status !== "terminated")
.filter((a) => {
if (!assigneeSearch.trim()) return true;
const q = assigneeSearch.toLowerCase();
return a.name.toLowerCase().includes(q);
})
.map((a) => (
<button
key={a.id}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
a.id === issue.assigneeAgentId && "bg-accent"
)}
onClick={() => { onUpdate({ assigneeAgentId: a.id }); setAssigneeOpen(false); }}
>
{a.name}
</button>
))}
</PopoverContent>
</Popover>
{issue.assigneeAgentId && (
<Link
to={`/agents/${assignee.id}`}
className="hover:underline"
to={`/agents/${issue.assigneeAgentId}`}
className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-accent/50 transition-colors text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<Identity name={assignee.name} size="sm" />
<ArrowUpRight className="h-3 w-3" />
</Link>
) : (
<span className="text-sm text-muted-foreground">Unassigned</span>
)}
</PropertyRow>
{issue.projectId && (
<PropertyRow label="Project">
<PropertyRow label="Project">
<Popover open={projectOpen} onOpenChange={(open) => { setProjectOpen(open); if (!open) setProjectSearch(""); }}>
<PopoverTrigger asChild>
<button className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1 py-0.5 transition-colors">
{issue.projectId ? (
<span className="text-sm">{projectName(issue.projectId)}</span>
) : (
<>
<Hexagon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm text-muted-foreground">No project</span>
</>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-52 p-1" align="end">
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder="Search projects..."
value={projectSearch}
onChange={(e) => setProjectSearch(e.target.value)}
autoFocus
/>
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
!issue.projectId && "bg-accent"
)}
onClick={() => { onUpdate({ projectId: null }); setProjectOpen(false); }}
>
No project
</button>
{(projects ?? [])
.filter((p) => {
if (!projectSearch.trim()) return true;
const q = projectSearch.toLowerCase();
return p.name.toLowerCase().includes(q);
})
.map((p) => (
<button
key={p.id}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
p.id === issue.projectId && "bg-accent"
)}
onClick={() => { onUpdate({ projectId: p.id }); setProjectOpen(false); }}
>
{p.name}
</button>
))}
</PopoverContent>
</Popover>
{issue.projectId && (
<Link
to={`/projects/${issue.projectId}`}
className="text-sm hover:underline"
className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-accent/50 transition-colors text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
{projectName(issue.projectId)}
<ArrowUpRight className="h-3 w-3" />
</Link>
</PropertyRow>
)}
)}
</PropertyRow>
{issue.parentId && (
<PropertyRow label="Parent">