refactor(conversation-status): share sidebar status glyphs across menus, search, and manage dialog
This commit is contained in:
@@ -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>
|
||||
|
||||
105
src/components/conversations/conversation-status-icon.tsx
Normal file
105
src/components/conversations/conversation-status-icon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
<ContextMenuItem
|
||||
key={s}
|
||||
onSelect={() => onStatusChange(conversation.id, s)}
|
||||
>
|
||||
<StatusIcon
|
||||
className={cn("h-4 w-4", STATUS_ICON_COLORS[s])}
|
||||
/>
|
||||
{tStatus(s)}
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
(s) => (
|
||||
<ContextMenuItem
|
||||
key={s}
|
||||
onSelect={() => onStatusChange(conversation.id, s)}
|
||||
>
|
||||
<ConversationStatusIcon
|
||||
status={s}
|
||||
className={cn("h-4 w-4", STATUS_ICON_COLORS[s])}
|
||||
/>
|
||||
{tStatus(s)}
|
||||
</ContextMenuItem>
|
||||
)
|
||||
)}
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user