refactor(conversation-status): share sidebar status glyphs across menus, search, and manage dialog

This commit is contained in:
xintaofei
2026-04-23 11:35:38 +08:00
parent f9dfc64009
commit bcd457c823
6 changed files with 176 additions and 240 deletions

View File

@@ -61,10 +61,11 @@ import type {
import {
AGENT_LABELS,
ALL_AGENT_TYPES,
STATUS_COLORS,
STATUS_ICON_COLORS,
STATUS_ORDER,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { ConversationStatusIcon } from "@/components/conversations/conversation-status-icon"
interface ConversationManageDialogProps {
open: boolean
@@ -291,11 +292,9 @@ export function ConversationManageDialog({
{STATUS_ORDER.map((s) => (
<SelectItem key={s} value={s}>
<span className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 rounded-full",
STATUS_COLORS[s]
)}
<ConversationStatusIcon
status={s}
className={cn("h-4 w-4", STATUS_ICON_COLORS[s])}
/>
{tStatus(s)}
</span>
@@ -388,11 +387,7 @@ export function ConversationManageDialog({
{formatRelative(conv.updated_at)}
</span>
<span
className={cn(
"shrink-0 h-2 w-2 rounded-full",
STATUS_COLORS[conv.status as ConversationStatus] ??
"bg-gray-400"
)}
className="shrink-0 inline-flex"
title={
STATUS_ORDER.includes(
conv.status as ConversationStatus
@@ -400,7 +395,17 @@ export function ConversationManageDialog({
? tStatus(conv.status as ConversationStatus)
: conv.status
}
>
<ConversationStatusIcon
status={conv.status as ConversationStatus}
className={cn(
"h-4 w-4",
STATUS_ICON_COLORS[
conv.status as ConversationStatus
] ?? "text-muted-foreground"
)}
/>
</span>
</div>
)
})
@@ -434,11 +439,9 @@ export function ConversationManageDialog({
key={s}
onSelect={() => handleBulkStatus(s)}
>
<span
className={cn(
"h-2 w-2 rounded-full mr-2",
STATUS_COLORS[s]
)}
<ConversationStatusIcon
status={s}
className={cn("h-4 w-4", STATUS_ICON_COLORS[s])}
/>
{tStatus(s)}
</DropdownMenuItem>

View File

@@ -0,0 +1,105 @@
"use client"
import type { ConversationStatus } from "@/lib/types"
import { cn } from "@/lib/utils"
interface ConversationStatusIconProps {
status: ConversationStatus
className?: string
}
export function ConversationStatusIcon({
status,
className,
}: ConversationStatusIconProps) {
return (
<svg
className={cn("shrink-0", className)}
width="1em"
height="1em"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
aria-hidden
>
{status === "in_progress" ? (
<>
<circle
cx="5"
cy="5"
r="3.8"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
opacity="0.28"
/>
<path
d="M5 1.2 A 3.8 3.8 0 1 1 1.2 5"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 5 5"
to="360 5 5"
dur="1.1s"
repeatCount="indefinite"
/>
</path>
</>
) : status === "pending_review" ? (
<>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
opacity="0.35"
/>
<circle cx="5" cy="5" r="2" fill="currentColor" />
</>
) : status === "cancelled" ? (
<>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
/>
<path
d="M3.4 3.4L6.6 6.6M6.6 3.4L3.4 6.6"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
/>
</>
) : (
<>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
/>
<path
d="M3.2 5.1 L4.4 6.3 L6.9 3.6"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
)}
</svg>
)
}

View File

@@ -17,8 +17,9 @@ import type {
DbConversationSummary,
} from "@/lib/types"
import { useFileTree, type FlatFileEntry } from "@/hooks/use-file-tree"
import { AGENT_LABELS, STATUS_COLORS, compareAgentType } from "@/lib/types"
import { AGENT_LABELS, STATUS_ICON_COLORS, compareAgentType } from "@/lib/types"
import { AgentIcon } from "@/components/agent-icon"
import { ConversationStatusIcon } from "@/components/conversations/conversation-status-icon"
import {
CommandDialog,
CommandInput,
@@ -268,11 +269,12 @@ export function SearchCommandDialog({
value={`${conv.id}-${conv.title ?? ""}`}
onSelect={() => handleSelectConversation(conv)}
>
<span
<ConversationStatusIcon
status={conv.status as ConversationStatus}
className={cn(
"w-2 h-2 rounded-full shrink-0",
STATUS_COLORS[conv.status as ConversationStatus] ??
"bg-gray-400"
"h-4 w-4",
STATUS_ICON_COLORS[conv.status as ConversationStatus] ??
"text-muted-foreground"
)}
/>
<span className="flex-1 truncate">

View File

@@ -1,20 +1,10 @@
"use client"
import { memo, useState, useCallback } from "react"
import {
Pencil,
Trash2,
Circle,
CircleAlert,
CircleCheck,
CircleDashed,
CircleX,
Plus,
type LucideIcon,
} from "lucide-react"
import { Pencil, Trash2, Circle, Plus } from "lucide-react"
import { useTranslations } from "next-intl"
import type { DbConversationSummary, ConversationStatus } from "@/lib/types"
import { STATUS_ORDER } from "@/lib/types"
import { STATUS_ICON_COLORS, STATUS_ORDER } from "@/lib/types"
import { cn } from "@/lib/utils"
import {
ContextMenu,
@@ -45,24 +35,8 @@ import {
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
SidebarStatusIcon,
conversationStatusToBead,
} from "./sidebar-status-icon"
const STATUS_ICONS: Record<ConversationStatus, LucideIcon> = {
in_progress: CircleDashed,
pending_review: CircleAlert,
completed: CircleCheck,
cancelled: CircleX,
}
const STATUS_ICON_COLORS: Record<ConversationStatus, string> = {
in_progress: "text-blue-500",
pending_review: "text-orange-500",
completed: "text-green-500",
cancelled: "text-red-500",
}
import { ConversationStatusIcon } from "./conversation-status-icon"
import { SidebarStatusIcon } from "./sidebar-status-icon"
interface SidebarConversationCardProps {
conversation: DbConversationSummary
@@ -123,7 +97,6 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
}, [conversation.id, conversation.agent_type, onDelete])
const status = conversation.status as ConversationStatus
const beadStatus = conversationStatusToBead(conversation.status)
const isRunning = status === "in_progress"
const isFailed = status === "cancelled"
@@ -165,7 +138,7 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
transform: "translateX(-50%)",
}}
/>
<SidebarStatusIcon status={beadStatus} emphasized={isOpenInTab} />
<SidebarStatusIcon status={status} emphasized={isOpenInTab} />
<span
className={cn(
@@ -238,20 +211,18 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
</ContextMenuSubTrigger>
<ContextMenuSubContent>
{STATUS_ORDER.filter((s) => s !== conversation.status).map(
(s) => {
const StatusIcon = STATUS_ICONS[s]
return (
(s) => (
<ContextMenuItem
key={s}
onSelect={() => onStatusChange(conversation.id, s)}
>
<StatusIcon
<ConversationStatusIcon
status={s}
className={cn("h-4 w-4", STATUS_ICON_COLORS[s])}
/>
{tStatus(s)}
</ContextMenuItem>
)
}
)}
</ContextMenuSubContent>
</ContextMenuSub>

View File

@@ -1,24 +1,29 @@
"use client"
import type { ConversationStatus } from "@/lib/types"
import { cn } from "@/lib/utils"
export type SidebarBeadStatus = "done" | "active" | "running" | "failed"
import { ConversationStatusIcon } from "./conversation-status-icon"
interface SidebarStatusIconProps {
status: SidebarBeadStatus
status: ConversationStatus
emphasized?: boolean
className?: string
}
function IconFrame({
children,
colorClass,
export function SidebarStatusIcon({
status,
emphasized = false,
className,
}: {
children: React.ReactNode
colorClass: string
className?: string
}) {
}: SidebarStatusIconProps) {
const colorClass =
status === "completed"
? emphasized
? "text-sidebar-primary/75"
: "text-sidebar-primary/40"
: emphasized
? "text-sidebar-primary"
: "text-sidebar-primary/65"
return (
<div
className={cn(
@@ -35,167 +40,10 @@ function IconFrame({
}}
aria-hidden
>
{children}
<ConversationStatusIcon
status={status}
className="h-[0.75rem] w-[0.75rem]"
/>
</div>
)
}
export function SidebarStatusIcon({
status,
emphasized = false,
className,
}: SidebarStatusIconProps) {
if (status === "running") {
return (
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.8"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
opacity="0.28"
/>
<path
d="M5 1.2 A 3.8 3.8 0 1 1 1.2 5"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 5 5"
to="360 5 5"
dur="1.1s"
repeatCount="indefinite"
/>
</path>
</svg>
</IconFrame>
)
}
if (status === "failed") {
return (
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
/>
<path
d="M3.4 3.4L6.6 6.6M6.6 3.4L3.4 6.6"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
/>
</svg>
</IconFrame>
)
}
if (status === "active") {
return (
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
opacity="0.35"
/>
<circle cx="5" cy="5" r="2" fill="currentColor" />
</svg>
</IconFrame>
)
}
return (
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary/75" : "text-sidebar-primary/40"
}
className={className}
>
<svg
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
/>
<path
d="M3.2 5.1 L4.4 6.3 L6.9 3.6"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</IconFrame>
)
}
export function conversationStatusToBead(status: string): SidebarBeadStatus {
switch (status) {
case "in_progress":
return "running"
case "pending_review":
return "active"
case "cancelled":
return "failed"
case "completed":
default:
return "done"
}
}

View File

@@ -228,6 +228,13 @@ export const STATUS_COLORS: Record<ConversationStatus, string> = {
cancelled: "bg-red-500",
}
export const STATUS_ICON_COLORS: Record<ConversationStatus, string> = {
in_progress: "text-blue-500",
pending_review: "text-orange-500",
completed: "text-green-500",
cancelled: "text-red-500",
}
export const AGENT_DISPLAY_ORDER: AgentType[] = [
"codex",
"claude_code",