Files
PaperClipAI/ui/src/components/AgentChatPicker.tsx
T
DottaandPaperclip 5282cabde8 fix(ui): make every agent reachable from Chats (#13420)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Experimental Agent Chat gives each person a persistent conversation
with each agent.
> - The sidebar only showed starred or previously visited agents.
> - A person could not start a conversation with an agent absent from
that list.
> - This pull request adds a searchable agent picker and a default
sidebar entry.
> - People can now reach every company agent while keeping their
frequent conversations close.

## Linked Issues or Issue Description

Refs #13283, which introduced the existing task-backed agent chat
surface. This change adds discovery to that surface after maintainer
review of the Storybook design.

**What happened?**

With Agent Chat enabled, a person with no starred or recent agents had
no agent shortcut. The sidebar also lacked a direct way to start a chat
with another agent.

**Expected behavior**

Show the first-created company agent by default. Let the person search
all company agents and open a persistent conversation.

**Steps to reproduce**

1. Enable Agent Chat in a company with agents.
2. Use an account with no starred agents or recent chats.
3. Look for a chat entry in the sidebar, or try to chat with an agent
absent from its shortcuts.

**Paperclip version or commit**

The existing behavior is present on master at 728f7185f.

## What Changed

- Add a compact picker that searches every company agent by name or
role. Include loading, retry, empty, and no-match states.
- Keep the first-created agent in Chats alongside stars and up to four
other recent conversations.
- Align the compose button with stars. Show it on hover or keyboard
focus, and keep it visible on touch devices.
- Use the existing company-aware chat route. Close the mobile sidebar on
selection and reset the picker when the company or user changes.
- Connect the approved Storybook scenarios to the production sidebar and
picker. Update product and design-guide documentation.

## Verification

- Ten focused tests pass for ordering, duplicate names, role search,
keyboard selection, search reset, retry states, full-roster navigation,
mobile dismissal, and company/user changes.
- The agent-chat sidebar end-to-end test passes against an isolated
PostgreSQL container. It verifies the first-use default, picker
navigation, lazy creation, stars, draft preservation, and reopening
existing history. The old four-row and See all link expectations now
match the approved behavior.
- Browser review of the production components in Storybook: open the
sidebar picker, search for an agent absent from the shortcuts, select
with Enter, and send a message through the existing composer.
- `pnpm build`, `pnpm -r typecheck`, `pnpm check:token-gates`, and `pnpm
build-storybook` pass on the rebased branch. The full local test run
reached 12,020 passing server tests, then stopped because an unrelated
environment-custom-images suite could not allocate PostgreSQL shared
memory on this Mac. All corresponding CI test shards pass on isolated
hosts.
- All CI checks pass on commit
`c324568324a6a8b65adfc6447ef19bcc710c61df`. Greptile reviewed that
commit at 5/5 with no open findings.
- Reviewer path: enable Agent Chat, open the Chats compose button,
search by name or role, and select an agent. Existing conversation
history must remain available.

## Risks

- Sidebar ordering changes: the earliest-created agent remains visible
even without a visit or star.
- The hover control remains available to keyboard and touch users.
- This change uses the existing feature gate, APIs, membership
preferences, and persistent conversation behavior. It adds no migration
or server contract.
- Storybook uses fixture responses. Its first-use landing page is a
review fixture and is not added as a product route.

## Model Used

OpenAI GPT-6 Astra (`gpt-6-astra`) through Codex, with reasoning, code
execution, and browser tools. The session does not expose its context
window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-14 13:19:13 -05:00

88 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import type { Agent } from "@paperclipai/shared";
import { AgentIcon } from "@/components/AgentIconPicker";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
export interface AgentChatPickerProps {
agents: Agent[];
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (agent: Agent) => void;
loading?: boolean;
error?: Error | null;
onRetry?: () => void;
}
export function AgentChatPicker({ open, onOpenChange, ...props }: AgentChatPickerProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined} className="gap-0 overflow-hidden p-0 sm:max-w-md">
<div className="px-4 pt-4 pb-3">
<DialogTitle>Chat with an agent</DialogTitle>
</div>
{/* The dialog unmounts its content on close, so each search starts empty. */}
<AgentChatPickerResults {...props} onSelect={(agent) => {
onOpenChange(false);
props.onSelect(agent);
}} />
</DialogContent>
</Dialog>
);
}
function AgentChatPickerResults({ agents, onSelect, loading, error, onRetry }: Omit<AgentChatPickerProps, "open" | "onOpenChange">) {
const [search, setSearch] = useState("");
return (
<Command>
<CommandInput
aria-label="Search agents by name or role"
placeholder="Search by name or role…"
value={search}
onValueChange={setSearch}
/>
{error ? (
<div role="alert" className="flex flex-col items-start gap-2 p-4 text-sm">
<p>Couldn’t load agents. Try again.</p>
{onRetry && <Button variant="outline" size="sm" onClick={onRetry}>Retry</Button>}
</div>
) : loading ? (
<p role="status" className="p-4 text-sm text-muted-foreground">Loading agents…</p>
) : (
<CommandList>
<CommandEmpty>
<div className="flex flex-col items-center gap-2 px-4">
<span>{agents.length ? `No agents match “${search}”` : "No agents yet."}</span>
{agents.length ? <>
<span className="text-xs text-muted-foreground">Try another name or role.</span>
<Button variant="ghost" size="sm" onClick={() => setSearch("")}>Clear search</Button>
</> : <span className="text-xs text-muted-foreground">Create an agent from the Agents page to start chatting.</span>}
</div>
</CommandEmpty>
<CommandGroup>
{agents.map((agent) => (
<CommandItem
key={agent.id}
value={agent.id}
keywords={[agent.name, agent.title ?? "", agent.role]}
onSelect={() => onSelect(agent)}
className="gap-3 px-3 py-3"
>
<AgentIcon icon={agent.icon} className="size-4 shrink-0" />
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-medium">{agent.name}</span>
<span className="truncate text-xs text-muted-foreground">{agent.title ?? agent.role}</span>
</span>
{agent.status === "paused" && <span className="text-xs text-(--status-agent-paused)">Paused</span>}
{agent.status === "terminated" && <span className="text-xs text-muted-foreground">Terminated</span>}
{agent.status === "pending_approval" && <span className="text-xs text-muted-foreground">Awaiting approval</span>}
</CommandItem>
))}
</CommandGroup>
</CommandList>
)}
</Command>
);
}