feat(coding): nest Conversations under Agents

This commit is contained in:
2026-08-25 16:34:11 +08:00
parent c4ca16fa19
commit 45d933732a
5 changed files with 238 additions and 64 deletions

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Bot,
ChevronRight,
CircleAlert,
LoaderCircle,
MessageSquarePlus,
@@ -558,74 +559,106 @@ export function CodingChatPanel({
<p className="mt-0.5 text-xs text-muted-foreground"> Conversation</p>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-2">
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground"></p>
<div className="space-y-1">
{agents.map((agent) => (
<button
key={agent.id}
type="button"
className={cn(
'flex min-h-10 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
selectedAgent?.id === agent.id ? 'bg-background font-medium shadow-soft' : 'hover:bg-background/70',
)}
onClick={() => {
clearConversationSelection();
selectAgent(agent.id);
}}
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-foreground text-xs font-semibold text-background">
{agent.name.trim().slice(0, 1) || 'A'}
</span>
<span className="min-w-0 flex-1 truncate">{agent.name}</span>
</button>
))}
</div>
<div className="mt-4 flex min-h-10 items-center justify-between px-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground"></p>
<Button
type="button"
variant="ghost"
size="icon"
className="h-10 w-10 rounded-xl transition-transform duration-150 ease-out active:scale-[0.96]"
aria-label="新建对话"
disabled={!selectedAgent || Boolean(selectedAgent && creatingAgentIds[selectedAgent.id])}
onClick={() => void handleCreateConversation()}
>
{selectedAgent && creatingAgentIds[selectedAgent.id]
? <LoaderCircle className="h-4 w-4 animate-spin" aria-hidden="true" />
: <MessageSquarePlus className="h-4 w-4" aria-hidden="true" />}
</Button>
</div>
<div className="space-y-1">
{agentConversations.map((conversation) => {
const unread = conversationSummaries[conversation.id]?.unread ?? conversation.unread;
const summary = conversationSummaries[conversation.id];
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground"></p>
<div className="space-y-1.5">
{agents.map((agent) => {
const expanded = selectedAgent?.id === agent.id;
const conversationGroupId = `agent-conversations-${agent.id}`;
return (
<button
key={conversation.id}
type="button"
<div
key={agent.id}
className={cn(
'flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
targetConversationId === conversation.id
? 'bg-background font-medium shadow-soft'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
'rounded-2xl',
expanded && 'bg-background p-1 shadow-soft',
)}
onClick={() => handleSelectConversation(conversation)}
>
<span className="min-w-0 flex-1 truncate">{conversation.title}</span>
{summary && ['queued', 'running', 'retrying', 'compacting', 'aborting'].includes(summary.runStatus) && (
<LoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-brand" aria-label="对话正在运行" />
<button
type="button"
className={cn(
'flex min-h-10 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
expanded ? 'font-semibold' : 'hover:bg-background/70',
)}
aria-label={agent.name}
aria-expanded={expanded}
aria-controls={conversationGroupId}
onClick={() => {
clearConversationSelection();
selectAgent(agent.id);
}}
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-foreground text-xs font-semibold text-background" aria-hidden="true">
{agent.name.trim().slice(0, 1) || 'A'}
</span>
<span className="min-w-0 flex-1 truncate">{agent.name}</span>
<ChevronRight
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out',
expanded && 'rotate-90',
)}
aria-hidden="true"
/>
</button>
{expanded && (
<div
id={conversationGroupId}
role="group"
aria-label={`${agent.name} 的对话`}
className="mb-1 ml-3 mt-1 border-l border-foreground/10 pl-3 pr-1"
>
<div className="flex min-h-10 items-center justify-between pl-2">
<p className="min-w-0 truncate text-[11px] font-semibold text-muted-foreground">
{agent.name}
</p>
<Button
type="button"
variant="ghost"
size="icon"
className="h-10 w-10 rounded-xl transition-transform duration-150 ease-out active:scale-[0.96]"
aria-label="新建对话"
disabled={Boolean(creatingAgentIds[agent.id])}
onClick={() => void handleCreateConversation()}
>
{creatingAgentIds[agent.id]
? <LoaderCircle className="h-4 w-4 animate-spin" aria-hidden="true" />
: <MessageSquarePlus className="h-4 w-4" aria-hidden="true" />}
</Button>
</div>
<div className="space-y-1 pb-1">
{agentConversations.map((conversation) => {
const unread = conversationSummaries[conversation.id]?.unread ?? conversation.unread;
const summary = conversationSummaries[conversation.id];
return (
<button
key={conversation.id}
type="button"
className={cn(
'flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
targetConversationId === conversation.id
? 'bg-surface-subtle font-medium'
: 'text-muted-foreground hover:bg-surface-subtle/70 hover:text-foreground',
)}
onClick={() => handleSelectConversation(conversation)}
>
<span className="min-w-0 flex-1 truncate">{conversation.title}</span>
{summary && ['queued', 'running', 'retrying', 'compacting', 'aborting'].includes(summary.runStatus) && (
<LoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-brand" aria-label="对话正在运行" />
)}
{unread && <span className="h-2 w-2 shrink-0 rounded-full bg-brand" aria-label="未读" />}
</button>
);
})}
{autoCreating && (
<div className="flex min-h-10 items-center gap-2 px-3 text-xs text-muted-foreground">
<LoaderCircle className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
</div>
)}
</div>
</div>
)}
{unread && <span className="h-2 w-2 shrink-0 rounded-full bg-brand" aria-label="未读" />}
</button>
</div>
);
})}
{autoCreating && (
<div className="flex min-h-10 items-center gap-2 px-3 text-xs text-muted-foreground">
<LoaderCircle className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
</div>
)}
</div>
</div>
</aside>