mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
## 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>
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
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>
|
||
);
|
||
}
|