Add ILIKE-based issue search across title, identifier, description, and comments with relevance ranking. Add assigneeUserId filter and allow agents to return issues to creator. Show assigned issue count in sidebar badges. Add minCount param to live-runs endpoint. Add activity charts (run activity, priority, status, success rate) to dashboard. Improve active agents panel with recent run cards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
229 lines
7.3 KiB
TypeScript
229 lines
7.3 KiB
TypeScript
import { useState, useEffect, useMemo } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { issuesApi } from "../api/issues";
|
|
import { agentsApi } from "../api/agents";
|
|
import { projectsApi } from "../api/projects";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
CommandSeparator,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
CircleDot,
|
|
Bot,
|
|
Hexagon,
|
|
Target,
|
|
LayoutDashboard,
|
|
Inbox,
|
|
DollarSign,
|
|
History,
|
|
SquarePen,
|
|
Plus,
|
|
} from "lucide-react";
|
|
import { Identity } from "./Identity";
|
|
|
|
export function CommandPalette() {
|
|
const [open, setOpen] = useState(false);
|
|
const [query, setQuery] = useState("");
|
|
const navigate = useNavigate();
|
|
const { selectedCompanyId } = useCompany();
|
|
const { openNewIssue, openNewAgent } = useDialog();
|
|
const searchQuery = query.trim();
|
|
|
|
useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
setOpen(true);
|
|
}
|
|
}
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!open) setQuery("");
|
|
}, [open]);
|
|
|
|
const { data: issues = [] } = useQuery({
|
|
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
|
queryFn: () => issuesApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && open,
|
|
});
|
|
|
|
const { data: searchedIssues = [] } = useQuery({
|
|
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery),
|
|
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery }),
|
|
enabled: !!selectedCompanyId && open && searchQuery.length > 0,
|
|
});
|
|
|
|
const { data: agents = [] } = useQuery({
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && open,
|
|
});
|
|
|
|
const { data: projects = [] } = useQuery({
|
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && open,
|
|
});
|
|
|
|
function go(path: string) {
|
|
setOpen(false);
|
|
navigate(path);
|
|
}
|
|
|
|
const agentName = (id: string | null) => {
|
|
if (!id) return null;
|
|
return agents.find((a) => a.id === id)?.name ?? null;
|
|
};
|
|
|
|
const visibleIssues = useMemo(
|
|
() => (searchQuery.length > 0 ? searchedIssues : issues),
|
|
[issues, searchedIssues, searchQuery],
|
|
);
|
|
|
|
return (
|
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
<CommandInput
|
|
placeholder="Search issues, agents, projects..."
|
|
value={query}
|
|
onValueChange={setQuery}
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
|
|
<CommandGroup heading="Actions">
|
|
<CommandItem
|
|
onSelect={() => {
|
|
setOpen(false);
|
|
openNewIssue();
|
|
}}
|
|
>
|
|
<SquarePen className="mr-2 h-4 w-4" />
|
|
Create new issue
|
|
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
|
</CommandItem>
|
|
<CommandItem
|
|
onSelect={() => {
|
|
setOpen(false);
|
|
openNewAgent();
|
|
}}
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create new agent
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/projects")}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create new project
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
<CommandSeparator />
|
|
|
|
<CommandGroup heading="Pages">
|
|
<CommandItem onSelect={() => go("/dashboard")}>
|
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
|
Dashboard
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/inbox")}>
|
|
<Inbox className="mr-2 h-4 w-4" />
|
|
Inbox
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/issues")}>
|
|
<CircleDot className="mr-2 h-4 w-4" />
|
|
Issues
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/projects")}>
|
|
<Hexagon className="mr-2 h-4 w-4" />
|
|
Projects
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/goals")}>
|
|
<Target className="mr-2 h-4 w-4" />
|
|
Goals
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/agents")}>
|
|
<Bot className="mr-2 h-4 w-4" />
|
|
Agents
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/costs")}>
|
|
<DollarSign className="mr-2 h-4 w-4" />
|
|
Costs
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/activity")}>
|
|
<History className="mr-2 h-4 w-4" />
|
|
Activity
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
{visibleIssues.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Issues">
|
|
{visibleIssues.slice(0, 10).map((issue) => (
|
|
<CommandItem
|
|
key={issue.id}
|
|
value={
|
|
searchQuery.length > 0
|
|
? `${searchQuery} ${issue.identifier ?? ""} ${issue.title} ${issue.description ?? ""}`
|
|
: undefined
|
|
}
|
|
keywords={issue.description ? [issue.description] : undefined}
|
|
onSelect={() => go(`/issues/${issue.identifier ?? issue.id}`)}
|
|
>
|
|
<CircleDot className="mr-2 h-4 w-4" />
|
|
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
|
</span>
|
|
<span className="flex-1 truncate">{issue.title}</span>
|
|
{issue.assigneeAgentId && (() => {
|
|
const name = agentName(issue.assigneeAgentId);
|
|
return name ? <Identity name={name} size="sm" className="ml-2" /> : null;
|
|
})()}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
|
|
{agents.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Agents">
|
|
{agents.slice(0, 10).map((agent) => (
|
|
<CommandItem key={agent.id} onSelect={() => go(`/agents/${agent.id}`)}>
|
|
<Bot className="mr-2 h-4 w-4" />
|
|
{agent.name}
|
|
<span className="text-xs text-muted-foreground ml-2">{agent.role}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
|
|
{projects.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Projects">
|
|
{projects.slice(0, 10).map((project) => (
|
|
<CommandItem key={project.id} onSelect={() => go(`/projects/${project.id}`)}>
|
|
<Hexagon className="mr-2 h-4 w-4" />
|
|
{project.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
</CommandList>
|
|
</CommandDialog>
|
|
);
|
|
}
|