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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user