mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-24 14:31:33 +00:00
refactor: queue primitive, tool status badges, and tool display cleanup
- Add Queue/QueueItem/QueueItemIndicator/QueueItemContent primitive - Rewrite TodoList using Queue (clean list, no bordered cards, no priority badges) - Add ToolStatusBadge component (Running/Completed/Error/Denied) - Migrate CollapsibleSection from native <details> to Collapsible primitive - Add badge prop threading through CollapsibleDisplay and CollapsibleSection - Add status badges to OneLineDisplay and CollapsibleDisplay via ToolRenderer - Update SubagentContainer: theme tokens + Collapsible for tool history - Replace hardcoded gray-* colors with theme tokens throughout tool displays
This commit is contained in:
@@ -122,8 +122,28 @@ export default function AppContent() {
|
||||
}
|
||||
}, [isConnected, selectedSession?.id, sendMessage]);
|
||||
|
||||
// Adjust the app container to stay above the virtual keyboard on iOS Safari.
|
||||
// On Chrome for Android the layout viewport already shrinks when the keyboard opens,
|
||||
// so inset-0 adjusts automatically. On iOS the layout viewport stays full-height and
|
||||
// the keyboard overlays it — we use the Visual Viewport API to track keyboard height
|
||||
// and apply it as a CSS variable that shifts the container's bottom edge up.
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const update = () => {
|
||||
const kb = Math.max(0, window.innerHeight - (vv.offsetTop + vv.height));
|
||||
document.documentElement.style.setProperty('--keyboard-height', `${kb}px`);
|
||||
};
|
||||
vv.addEventListener('resize', update);
|
||||
vv.addEventListener('scroll', update);
|
||||
return () => {
|
||||
vv.removeEventListener('resize', update);
|
||||
vv.removeEventListener('scroll', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex bg-background">
|
||||
<div className="fixed inset-0 flex bg-background" style={{ bottom: 'var(--keyboard-height, 0px)' }}>
|
||||
{!isMobile ? (
|
||||
<div className="h-full flex-shrink-0 border-r border-border/50">
|
||||
<Sidebar {...sidebarSharedProps} />
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { SubagentChildTool } from '../types/types';
|
||||
import { getToolConfig } from './configs/toolConfigs';
|
||||
import { OneLineDisplay, CollapsibleDisplay, ToolDiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
|
||||
import { PlanDisplay } from './components/PlanDisplay';
|
||||
import { ToolStatusBadge } from './components/ToolStatusBadge';
|
||||
import type { ToolStatus } from './components/ToolStatusBadge';
|
||||
|
||||
type DiffLine = {
|
||||
type: string;
|
||||
@@ -39,12 +41,24 @@ function getToolCategory(toolName: string): string {
|
||||
if (toolName === 'Bash') return 'bash';
|
||||
if (['TodoWrite', 'TodoRead'].includes(toolName)) return 'todo';
|
||||
if (['TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'].includes(toolName)) return 'task';
|
||||
if (toolName === 'Task') return 'agent'; // Subagent task
|
||||
if (toolName === 'Task') return 'agent';
|
||||
if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') return 'plan';
|
||||
if (toolName === 'AskUserQuestion') return 'question';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function deriveToolStatus(toolResult: any): ToolStatus {
|
||||
if (!toolResult) return 'running';
|
||||
if (toolResult.isError) {
|
||||
const content = String(toolResult.content || '').toLowerCase();
|
||||
if (content.includes('permission') || content.includes('denied') || content.includes('not allowed')) {
|
||||
return 'denied';
|
||||
}
|
||||
return 'error';
|
||||
}
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main tool renderer router
|
||||
* Routes to OneLineDisplay or CollapsibleDisplay based on tool config
|
||||
@@ -76,6 +90,12 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
}
|
||||
}, [mode, toolInput, toolResult]);
|
||||
|
||||
// Only derive and show status badge on input renders
|
||||
const toolStatus = useMemo(
|
||||
() => mode === 'input' ? deriveToolStatus(toolResult) : undefined,
|
||||
[mode, toolResult],
|
||||
);
|
||||
|
||||
const handleAction = useCallback(() => {
|
||||
if (displayConfig?.action === 'open-file' && onFileOpen) {
|
||||
const value = displayConfig.getValue?.(parsedData) || '';
|
||||
@@ -85,9 +105,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
|
||||
// Route subagent containers to dedicated component (after hooks to satisfy Rules of Hooks)
|
||||
if (isSubagentContainer && subagentState) {
|
||||
if (mode === 'result') {
|
||||
return null;
|
||||
}
|
||||
if (mode === 'result') return null;
|
||||
return (
|
||||
<SubagentContainer
|
||||
toolInput={toolInput}
|
||||
@@ -118,6 +136,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
wrapText={displayConfig.wrapText}
|
||||
colorScheme={displayConfig.colorScheme}
|
||||
resultId={mode === 'input' ? `tool-result-${toolId}` : undefined}
|
||||
status={toolStatus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -164,7 +183,6 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
onFileOpen
|
||||
}) || {};
|
||||
|
||||
// Build the content component based on contentType
|
||||
let contentComponent: React.ReactNode = null;
|
||||
|
||||
switch (displayConfig.contentType) {
|
||||
@@ -241,7 +259,6 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
}
|
||||
}
|
||||
|
||||
// For edit tools, make the title (filename) clickable to open the file
|
||||
const handleTitleClick = (toolName === 'Edit' || toolName === 'Write' || toolName === 'ApplyPatch') && contentProps.filePath && onFileOpen
|
||||
? () => onFileOpen(contentProps.filePath, {
|
||||
old_string: contentProps.oldContent,
|
||||
@@ -249,6 +266,8 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const badgeElement = toolStatus ? <ToolStatusBadge status={toolStatus} /> : undefined;
|
||||
|
||||
return (
|
||||
<CollapsibleDisplay
|
||||
toolName={toolName}
|
||||
@@ -256,6 +275,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
title={title}
|
||||
defaultOpen={defaultOpen}
|
||||
onTitleClick={handleTitleClick}
|
||||
badge={badgeElement}
|
||||
showRawParameters={mode === 'input' && showRawParameters}
|
||||
rawContent={rawToolInput}
|
||||
toolCategory={getToolCategory(toolName)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../../shared/view/ui';
|
||||
import { CollapsibleSection } from './CollapsibleSection';
|
||||
|
||||
interface CollapsibleDisplayProps {
|
||||
@@ -7,6 +8,7 @@ interface CollapsibleDisplayProps {
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
action?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
onTitleClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
showRawParameters?: boolean;
|
||||
@@ -17,14 +19,14 @@ interface CollapsibleDisplayProps {
|
||||
|
||||
const borderColorMap: Record<string, string> = {
|
||||
edit: 'border-l-amber-500 dark:border-l-amber-400',
|
||||
search: 'border-l-gray-400 dark:border-l-gray-500',
|
||||
search: 'border-l-muted-foreground/40',
|
||||
bash: 'border-l-green-500 dark:border-l-green-400',
|
||||
todo: 'border-l-violet-500 dark:border-l-violet-400',
|
||||
task: 'border-l-violet-500 dark:border-l-violet-400',
|
||||
agent: 'border-l-purple-500 dark:border-l-purple-400',
|
||||
plan: 'border-l-indigo-500 dark:border-l-indigo-400',
|
||||
question: 'border-l-blue-500 dark:border-l-blue-400',
|
||||
default: 'border-l-gray-300 dark:border-l-gray-600',
|
||||
default: 'border-l-border',
|
||||
};
|
||||
|
||||
export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||
@@ -32,14 +34,14 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||
title,
|
||||
defaultOpen = false,
|
||||
action,
|
||||
badge,
|
||||
onTitleClick,
|
||||
children,
|
||||
showRawParameters = false,
|
||||
rawContent,
|
||||
className = '',
|
||||
toolCategory
|
||||
toolCategory,
|
||||
}) => {
|
||||
// Fall back to default styling for unknown/new categories so className never includes "undefined".
|
||||
const borderColor = borderColorMap[toolCategory || 'default'] || borderColorMap.default;
|
||||
|
||||
return (
|
||||
@@ -49,15 +51,16 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||
toolName={toolName}
|
||||
open={defaultOpen}
|
||||
action={action}
|
||||
badge={badge}
|
||||
onTitleClick={onTitleClick}
|
||||
>
|
||||
{children}
|
||||
|
||||
{showRawParameters && rawContent && (
|
||||
<details className="group/raw relative mt-2">
|
||||
<summary className="flex cursor-pointer items-center gap-1.5 py-0.5 text-[11px] text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
|
||||
<Collapsible className="mt-2">
|
||||
<CollapsibleTrigger className="flex items-center gap-1.5 py-0.5 text-[11px] text-muted-foreground hover:text-foreground">
|
||||
<svg
|
||||
className="h-2.5 w-2.5 transition-transform duration-150 group-open/raw:rotate-90"
|
||||
className="h-2.5 w-2.5 flex-shrink-0 transition-transform duration-150 data-[state=open]:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -65,11 +68,13 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
raw params
|
||||
</summary>
|
||||
<pre className="mt-1 overflow-hidden whitespace-pre-wrap break-words rounded border border-gray-200/40 bg-gray-50 p-2 font-mono text-[11px] text-gray-600 dark:border-gray-700/40 dark:bg-gray-900/50 dark:text-gray-400">
|
||||
{rawContent}
|
||||
</pre>
|
||||
</details>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre className="mt-1 overflow-hidden whitespace-pre-wrap break-words rounded border border-border/40 bg-muted p-2 font-mono text-[11px] text-muted-foreground">
|
||||
{rawContent}
|
||||
</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../../shared/view/ui';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
toolName?: string;
|
||||
open?: boolean;
|
||||
action?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
onTitleClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
@@ -18,44 +21,68 @@ export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||
toolName,
|
||||
open = false,
|
||||
action,
|
||||
badge,
|
||||
onTitleClick,
|
||||
children,
|
||||
className = ''
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<details className={`group/details relative ${className}`} open={open}>
|
||||
<summary className="flex cursor-pointer select-none items-center gap-1.5 py-0.5 text-xs group-open/details:sticky group-open/details:top-0 group-open/details:z-10 group-open/details:-mx-1 group-open/details:bg-background group-open/details:px-1">
|
||||
<svg
|
||||
className="h-3 w-3 flex-shrink-0 text-gray-400 transition-transform duration-150 group-open/details:rotate-90 dark:text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">{toolName}</span>
|
||||
)}
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 text-[10px] text-gray-300 dark:text-gray-600">/</span>
|
||||
)}
|
||||
{onTitleClick ? (
|
||||
<Collapsible defaultOpen={open} className={cn('group/section', className)}>
|
||||
{/* When there's a clickable title (Edit/Write), only the chevron toggles collapse */}
|
||||
{onTitleClick ? (
|
||||
<div className="flex cursor-default select-none items-center gap-1.5 py-0.5 text-xs group-data-[state=open]/section:sticky group-data-[state=open]/section:top-0 group-data-[state=open]/section:z-10 group-data-[state=open]/section:-mx-1 group-data-[state=open]/section:bg-background group-data-[state=open]/section:px-1">
|
||||
<CollapsibleTrigger className="flex flex-shrink-0 items-center p-0.5 text-muted-foreground hover:text-foreground">
|
||||
<svg
|
||||
className="h-3 w-3 transition-transform duration-150 group-data-[state=open]/section:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</CollapsibleTrigger>
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 font-medium text-muted-foreground">{toolName}</span>
|
||||
)}
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 text-[10px] text-muted-foreground/40">/</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onTitleClick(); }}
|
||||
className="flex-1 truncate text-left font-mono text-blue-600 transition-colors hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
||||
onClick={onTitleClick}
|
||||
className="flex-1 truncate text-left font-mono text-primary transition-colors hover:text-primary/80 hover:underline"
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
) : (
|
||||
<span className="flex-1 truncate text-gray-600 dark:text-gray-400">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{action && <span className="ml-1 flex-shrink-0">{action}</span>}
|
||||
</summary>
|
||||
<div className="mt-1.5 pl-[18px]">
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
{badge && <span className="ml-auto flex-shrink-0">{badge}</span>}
|
||||
{action && <span className="ml-1 flex-shrink-0">{action}</span>}
|
||||
</div>
|
||||
) : (
|
||||
<CollapsibleTrigger className="flex w-full select-none items-center gap-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:text-foreground group-data-[state=open]/section:sticky group-data-[state=open]/section:top-0 group-data-[state=open]/section:z-10 group-data-[state=open]/section:-mx-1 group-data-[state=open]/section:bg-background group-data-[state=open]/section:px-1">
|
||||
<svg
|
||||
className="h-3 w-3 flex-shrink-0 transition-transform duration-150 group-data-[state=open]/section:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 font-medium">{toolName}</span>
|
||||
)}
|
||||
{toolName && (
|
||||
<span className="flex-shrink-0 text-[10px] text-muted-foreground/40">/</span>
|
||||
)}
|
||||
<span className="flex-1 truncate text-left">{title}</span>
|
||||
{badge && <span className="ml-auto flex-shrink-0">{badge}</span>}
|
||||
{action && <span className="ml-1 flex-shrink-0">{action}</span>}
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
|
||||
<CollapsibleContent>
|
||||
<div className="mt-1.5 pl-[18px]">
|
||||
{children}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,114 +1,21 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { CheckCircle2, Circle, Clock, type LucideIcon } from 'lucide-react';
|
||||
import { Badge } from '../../../../../shared/view/ui';
|
||||
|
||||
type TodoStatus = 'completed' | 'in_progress' | 'pending';
|
||||
type TodoPriority = 'high' | 'medium' | 'low';
|
||||
import { Queue, QueueItem, QueueItemIndicator, QueueItemContent } from '../../../../../shared/view/ui';
|
||||
import type { QueueItemStatus } from '../../../../../shared/view/ui';
|
||||
|
||||
export type TodoItem = {
|
||||
id?: string;
|
||||
content: string;
|
||||
status: string;
|
||||
priority?: string;
|
||||
activeForm?: string;
|
||||
};
|
||||
|
||||
type NormalizedTodoItem = {
|
||||
id?: string;
|
||||
content: string;
|
||||
status: TodoStatus;
|
||||
priority: TodoPriority;
|
||||
};
|
||||
|
||||
type StatusConfig = {
|
||||
icon: LucideIcon;
|
||||
iconClassName: string;
|
||||
badgeClassName: string;
|
||||
textClassName: string;
|
||||
};
|
||||
|
||||
// Centralized visual config keeps rendering logic compact and easier to scan.
|
||||
const STATUS_CONFIG: Record<TodoStatus, StatusConfig> = {
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
iconClassName: 'w-3.5 h-3.5 text-green-500 dark:text-green-400',
|
||||
badgeClassName:
|
||||
'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800',
|
||||
textClassName: 'line-through text-gray-500 dark:text-gray-400',
|
||||
},
|
||||
in_progress: {
|
||||
icon: Clock,
|
||||
iconClassName: 'w-3.5 h-3.5 text-blue-500 dark:text-blue-400',
|
||||
badgeClassName:
|
||||
'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800',
|
||||
textClassName: 'text-gray-900 dark:text-gray-100',
|
||||
},
|
||||
pending: {
|
||||
icon: Circle,
|
||||
iconClassName: 'w-3.5 h-3.5 text-gray-400 dark:text-gray-500',
|
||||
badgeClassName:
|
||||
'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700',
|
||||
textClassName: 'text-gray-900 dark:text-gray-100',
|
||||
},
|
||||
};
|
||||
|
||||
const PRIORITY_BADGE_CLASS: Record<TodoPriority, string> = {
|
||||
high: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800',
|
||||
medium:
|
||||
'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800',
|
||||
low: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700',
|
||||
};
|
||||
|
||||
// Incoming tool payloads can vary; normalize to supported UI states.
|
||||
const normalizeStatus = (status: string): TodoStatus => {
|
||||
if (status === 'completed' || status === 'in_progress') {
|
||||
return status;
|
||||
}
|
||||
const normalizeStatus = (status: string): QueueItemStatus => {
|
||||
if (status === 'completed') return 'completed';
|
||||
if (status === 'in_progress') return 'in_progress';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const normalizePriority = (priority?: string): TodoPriority => {
|
||||
if (priority === 'high' || priority === 'medium') {
|
||||
return priority;
|
||||
}
|
||||
return 'low';
|
||||
};
|
||||
|
||||
const TodoRow = memo(
|
||||
({ todo }: { todo: NormalizedTodoItem }) => {
|
||||
const statusConfig = STATUS_CONFIG[todo.status];
|
||||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2 rounded border border-gray-200 bg-white p-2 transition-colors dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<StatusIcon className={statusConfig.iconClassName} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-0.5 flex items-start justify-between gap-2">
|
||||
<p className={`text-xs font-medium ${statusConfig.textClassName}`}>
|
||||
{todo.content}
|
||||
</p>
|
||||
<div className="flex flex-shrink-0 gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-1.5 py-px text-[10px] ${PRIORITY_BADGE_CLASS[todo.priority]}`}
|
||||
>
|
||||
{todo.priority}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-1.5 py-px text-[10px] ${statusConfig.badgeClassName}`}
|
||||
>
|
||||
{todo.status.replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const TodoList = memo(
|
||||
({
|
||||
todos,
|
||||
@@ -117,36 +24,33 @@ const TodoList = memo(
|
||||
todos: TodoItem[];
|
||||
isResult?: boolean;
|
||||
}) => {
|
||||
// Memoize normalization to avoid recomputing list metadata on every render.
|
||||
const normalizedTodos = useMemo<NormalizedTodoItem[]>(
|
||||
() =>
|
||||
todos.map((todo) => ({
|
||||
id: todo.id,
|
||||
content: todo.content,
|
||||
status: normalizeStatus(todo.status),
|
||||
priority: normalizePriority(todo.priority),
|
||||
})),
|
||||
[todos]
|
||||
const normalized = useMemo(
|
||||
() => todos.map((todo) => ({ ...todo, queueStatus: normalizeStatus(todo.status) })),
|
||||
[todos],
|
||||
);
|
||||
|
||||
if (normalizedTodos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (normalized.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div>
|
||||
{isResult && (
|
||||
<div className="mb-1.5 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Todo List ({normalizedTodos.length}{' '}
|
||||
{normalizedTodos.length === 1 ? 'item' : 'items'})
|
||||
<div className="mb-1.5 text-xs font-medium text-muted-foreground">
|
||||
Todo List ({normalized.length} {normalized.length === 1 ? 'item' : 'items'})
|
||||
</div>
|
||||
)}
|
||||
{normalizedTodos.map((todo, index) => (
|
||||
<TodoRow key={todo.id ?? `${todo.content}-${index}`} todo={todo} />
|
||||
))}
|
||||
<Queue>
|
||||
{normalized.map((todo, index) => (
|
||||
<QueueItem key={todo.id ?? `${todo.content}-${index}`} status={todo.queueStatus}>
|
||||
<QueueItemIndicator />
|
||||
<QueueItemContent>{todo.content}</QueueItemContent>
|
||||
</QueueItem>
|
||||
))}
|
||||
</Queue>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
TodoList.displayName = 'TodoList';
|
||||
|
||||
export default TodoList;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { copyTextToClipboard } from '../../../../utils/clipboard';
|
||||
import { ToolStatusBadge } from './ToolStatusBadge';
|
||||
import type { ToolStatus } from './ToolStatusBadge';
|
||||
|
||||
type ActionType = 'copy' | 'open-file' | 'jump-to-results' | 'none';
|
||||
|
||||
@@ -23,6 +25,7 @@ interface OneLineDisplayProps {
|
||||
resultId?: string;
|
||||
toolResult?: any;
|
||||
toolId?: string;
|
||||
status?: ToolStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,14 +43,15 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
style,
|
||||
wrapText = false,
|
||||
colorScheme = {
|
||||
primary: 'text-gray-700 dark:text-gray-300',
|
||||
secondary: 'text-gray-500 dark:text-gray-400',
|
||||
primary: 'text-foreground',
|
||||
secondary: 'text-muted-foreground',
|
||||
background: '',
|
||||
border: 'border-gray-300 dark:border-gray-600',
|
||||
icon: 'text-gray-500 dark:text-gray-400'
|
||||
border: 'border-border',
|
||||
icon: 'text-muted-foreground',
|
||||
},
|
||||
toolResult,
|
||||
toolId
|
||||
toolId,
|
||||
status,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const isTerminal = style === 'terminal';
|
||||
@@ -55,9 +59,7 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
const handleAction = async () => {
|
||||
if (action === 'copy' && value) {
|
||||
const didCopy = await copyTextToClipboard(value);
|
||||
if (!didCopy) {
|
||||
return;
|
||||
}
|
||||
if (!didCopy) return;
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} else if (onAction) {
|
||||
@@ -68,7 +70,7 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
const renderCopyButton = () => (
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="ml-1 flex-shrink-0 text-gray-400 opacity-0 transition-all hover:text-gray-600 group-hover:opacity-100 dark:hover:text-gray-200"
|
||||
className="ml-1 flex-shrink-0 text-muted-foreground/40 opacity-0 transition-all hover:text-muted-foreground group-hover:opacity-100"
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
@@ -84,7 +86,7 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
// Terminal style: dark pill only around the command
|
||||
// Terminal style: dark pill around the command
|
||||
if (isTerminal) {
|
||||
return (
|
||||
<div className="group my-1">
|
||||
@@ -100,12 +102,13 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
<span className="select-none text-green-600 dark:text-green-500">$ </span>{value}
|
||||
</code>
|
||||
</div>
|
||||
{status && <ToolStatusBadge status={status} className="mt-0.5" />}
|
||||
{action === 'copy' && renderCopyButton()}
|
||||
</div>
|
||||
</div>
|
||||
{secondary && (
|
||||
<div className="ml-7 mt-1">
|
||||
<span className="text-[11px] italic text-gray-400 dark:text-gray-500">
|
||||
<span className="text-[11px] italic text-muted-foreground/60">
|
||||
{secondary}
|
||||
</span>
|
||||
</div>
|
||||
@@ -114,20 +117,21 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// File open style - show filename only, full path on hover
|
||||
// File open style
|
||||
if (action === 'open-file') {
|
||||
const displayName = value.split('/').pop() || value;
|
||||
return (
|
||||
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} my-0.5 py-0.5 pl-3`}>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
|
||||
<span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
|
||||
<span className="flex-shrink-0 text-xs text-muted-foreground">{label || toolName}</span>
|
||||
<span className="text-[10px] text-muted-foreground/40">/</span>
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="truncate font-mono text-xs text-blue-600 transition-colors hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
||||
className="truncate font-mono text-xs text-primary transition-colors hover:text-primary/80 hover:underline"
|
||||
title={value}
|
||||
>
|
||||
{displayName}
|
||||
</button>
|
||||
{status && <ToolStatusBadge status={status} className="ml-auto" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -136,20 +140,21 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
if (action === 'jump-to-results') {
|
||||
return (
|
||||
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} my-0.5 py-0.5 pl-3`}>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
|
||||
<span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
|
||||
<span className="flex-shrink-0 text-xs text-muted-foreground">{label || toolName}</span>
|
||||
<span className="text-[10px] text-muted-foreground/40">/</span>
|
||||
<span className={`min-w-0 flex-1 truncate font-mono text-xs ${colorScheme.primary}`}>
|
||||
{value}
|
||||
</span>
|
||||
{secondary && (
|
||||
<span className="flex-shrink-0 text-[11px] italic text-gray-400 dark:text-gray-500">
|
||||
<span className="flex-shrink-0 text-[11px] italic text-muted-foreground/60">
|
||||
{secondary}
|
||||
</span>
|
||||
)}
|
||||
{status && <ToolStatusBadge status={status} />}
|
||||
{toolResult && (
|
||||
<a
|
||||
href={`#tool-result-${toolId}`}
|
||||
className="flex flex-shrink-0 items-center gap-0.5 text-[11px] text-blue-600 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
className="flex flex-shrink-0 items-center gap-0.5 text-[11px] text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
@@ -167,10 +172,10 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
<span className={`${colorScheme.icon} flex-shrink-0 text-xs`}>{icon}</span>
|
||||
)}
|
||||
{!icon && (label || toolName) && (
|
||||
<span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
|
||||
<span className="flex-shrink-0 text-xs text-muted-foreground">{label || toolName}</span>
|
||||
)}
|
||||
{(icon || label || toolName) && (
|
||||
<span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
|
||||
<span className="text-[10px] text-muted-foreground/40">/</span>
|
||||
)}
|
||||
<span className={`font-mono text-xs ${wrapText ? 'whitespace-pre-wrap break-all' : 'truncate'} min-w-0 flex-1 ${colorScheme.primary}`}>
|
||||
{value}
|
||||
@@ -180,6 +185,7 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
||||
{secondary}
|
||||
</span>
|
||||
)}
|
||||
{status && <ToolStatusBadge status={status} />}
|
||||
{action === 'copy' && renderCopyButton()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { SubagentChildTool } from '../../types/types';
|
||||
import { CollapsibleSection } from './CollapsibleSection';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../../shared/view/ui';
|
||||
|
||||
interface SubagentContainerProps {
|
||||
toolInput: unknown;
|
||||
@@ -65,21 +66,21 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
|
||||
>
|
||||
{/* Prompt/request to the subagent */}
|
||||
{prompt && (
|
||||
<div className="mb-2 line-clamp-4 whitespace-pre-wrap break-words text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="mb-2 line-clamp-4 whitespace-pre-wrap break-words text-xs text-muted-foreground">
|
||||
{prompt}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current tool indicator (while running) */}
|
||||
{currentTool && !isComplete && (
|
||||
<div className="mt-1 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-purple-500 dark:bg-purple-400" />
|
||||
<span className="text-gray-400 dark:text-gray-500">Currently:</span>
|
||||
<span className="font-medium text-gray-600 dark:text-gray-300">{currentTool.toolName}</span>
|
||||
<span className="text-muted-foreground/60">Currently:</span>
|
||||
<span className="font-medium text-foreground">{currentTool.toolName}</span>
|
||||
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput) && (
|
||||
<>
|
||||
<span className="text-gray-300 dark:text-gray-600">/</span>
|
||||
<span className="truncate font-mono text-gray-500 dark:text-gray-400">
|
||||
<span className="text-muted-foreground/40">/</span>
|
||||
<span className="truncate font-mono text-muted-foreground">
|
||||
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput)}
|
||||
</span>
|
||||
</>
|
||||
@@ -99,10 +100,10 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
|
||||
|
||||
{/* Tool history (collapsed) */}
|
||||
{childTools.length > 0 && (
|
||||
<details className="group/history mt-2">
|
||||
<summary className="flex cursor-pointer items-center gap-1 text-[11px] text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
|
||||
<Collapsible className="mt-2">
|
||||
<CollapsibleTrigger className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground">
|
||||
<svg
|
||||
className="h-2.5 w-2.5 flex-shrink-0 transition-transform duration-150 group-open/history:rotate-90"
|
||||
className="h-2.5 w-2.5 flex-shrink-0 transition-transform duration-150 data-[state=open]:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -110,29 +111,31 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>View tool history ({childTools.length})</span>
|
||||
</summary>
|
||||
<div className="mt-1 space-y-0.5 border-l border-gray-200 pl-3 dark:border-gray-700">
|
||||
{childTools.map((child, index) => (
|
||||
<div key={child.toolId} className="flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400">
|
||||
<span className="w-4 flex-shrink-0 text-right text-gray-400 dark:text-gray-500">{index + 1}.</span>
|
||||
<span className="font-medium">{child.toolName}</span>
|
||||
{getCompactToolDisplay(child.toolName, child.toolInput) && (
|
||||
<span className="truncate font-mono text-gray-400 dark:text-gray-500">
|
||||
{getCompactToolDisplay(child.toolName, child.toolInput)}
|
||||
</span>
|
||||
)}
|
||||
{child.toolResult?.isError && (
|
||||
<span className="flex-shrink-0 text-red-500">(error)</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-1 space-y-0.5 border-l border-border pl-3">
|
||||
{childTools.map((child, index) => (
|
||||
<div key={child.toolId} className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span className="w-4 flex-shrink-0 text-right text-muted-foreground/60">{index + 1}.</span>
|
||||
<span className="font-medium text-foreground">{child.toolName}</span>
|
||||
{getCompactToolDisplay(child.toolName, child.toolInput) && (
|
||||
<span className="truncate font-mono text-muted-foreground/70">
|
||||
{getCompactToolDisplay(child.toolName, child.toolInput)}
|
||||
</span>
|
||||
)}
|
||||
{child.toolResult?.isError && (
|
||||
<span className="flex-shrink-0 text-red-500">(error)</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
|
||||
{/* Final result */}
|
||||
{isComplete && toolResult && (
|
||||
<div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{(() => {
|
||||
let content = toolResult.content;
|
||||
|
||||
|
||||
42
src/components/chat/tools/components/ToolStatusBadge.tsx
Normal file
42
src/components/chat/tools/components/ToolStatusBadge.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { cn } from '../../../../lib/utils';
|
||||
|
||||
export type ToolStatus = 'running' | 'completed' | 'error' | 'denied';
|
||||
|
||||
const STATUS_CONFIG: Record<ToolStatus, { label: string; className: string }> = {
|
||||
running: {
|
||||
label: 'Running',
|
||||
className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
},
|
||||
completed: {
|
||||
label: 'Completed',
|
||||
className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
},
|
||||
error: {
|
||||
label: 'Error',
|
||||
className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
},
|
||||
denied: {
|
||||
label: 'Denied',
|
||||
className: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
},
|
||||
};
|
||||
|
||||
interface ToolStatusBadgeProps {
|
||||
status: ToolStatus;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ToolStatusBadge({ status, className }: ToolStatusBadgeProps) {
|
||||
const config = STATUS_CONFIG[status];
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded px-1.5 py-px text-[10px] font-medium',
|
||||
config.className,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,5 @@ export { CollapsibleDisplay } from './CollapsibleDisplay';
|
||||
export { SubagentContainer } from './SubagentContainer';
|
||||
export * from './ContentRenderers';
|
||||
export * from './InteractiveRenderers';
|
||||
export { ToolStatusBadge } from './ToolStatusBadge';
|
||||
export type { ToolStatus } from './ToolStatusBadge';
|
||||
|
||||
@@ -98,7 +98,6 @@ interface ChatComposerProps {
|
||||
onTextareaScrollSync: (target: HTMLTextAreaElement) => void;
|
||||
onTextareaInput: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||
onInputFocusChange?: (focused: boolean) => void;
|
||||
isInputFocused?: boolean;
|
||||
placeholder: string;
|
||||
isTextareaExpanded: boolean;
|
||||
sendByCtrlEnter?: boolean;
|
||||
@@ -154,7 +153,6 @@ export default function ChatComposer({
|
||||
onTextareaScrollSync,
|
||||
onTextareaInput,
|
||||
onInputFocusChange,
|
||||
isInputFocused,
|
||||
placeholder,
|
||||
isTextareaExpanded,
|
||||
sendByCtrlEnter,
|
||||
@@ -175,13 +173,8 @@ export default function ChatComposer({
|
||||
// Hide the thinking/status bar while any permission request is pending
|
||||
const hasPendingPermissions = pendingPermissionRequests.length > 0;
|
||||
|
||||
// On mobile, when input is focused, float the input box at the bottom
|
||||
const mobileFloatingClass = isInputFocused
|
||||
? 'max-sm:fixed max-sm:bottom-0 max-sm:left-0 max-sm:right-0 max-sm:z-50 max-sm:bg-background max-sm:shadow-[0_-4px_20px_rgba(0,0,0,0.15)]'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className={`flex-shrink-0 p-2 pb-2 sm:p-4 sm:pb-4 md:p-4 md:pb-6 ${mobileFloatingClass}`}>
|
||||
<div className="flex-shrink-0 p-2 pb-2 sm:p-4 sm:pb-4 md:p-4 md:pb-6">
|
||||
{!hasPendingPermissions && (
|
||||
<ClaudeStatus
|
||||
status={claudeStatus}
|
||||
|
||||
122
src/shared/view/ui/Queue.tsx
Normal file
122
src/shared/view/ui/Queue.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
/* ─── Types ──────────────────────────────────────────────────────── */
|
||||
|
||||
export type QueueItemStatus = 'completed' | 'in_progress' | 'pending';
|
||||
|
||||
/* ─── Context ────────────────────────────────────────────────────── */
|
||||
|
||||
interface QueueItemContextValue {
|
||||
status: QueueItemStatus;
|
||||
}
|
||||
|
||||
const QueueItemContext = React.createContext<QueueItemContextValue | null>(null);
|
||||
|
||||
function useQueueItem() {
|
||||
const ctx = React.useContext(QueueItemContext);
|
||||
if (!ctx) throw new Error('QueueItem sub-components must be used within <QueueItem>');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/* ─── Queue ──────────────────────────────────────────────────────── */
|
||||
|
||||
export const Queue = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="queue"
|
||||
role="list"
|
||||
className={cn('space-y-0.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Queue.displayName = 'Queue';
|
||||
|
||||
/* ─── QueueItem ──────────────────────────────────────────────────── */
|
||||
|
||||
export interface QueueItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
status?: QueueItemStatus;
|
||||
}
|
||||
|
||||
export const QueueItem = React.forwardRef<HTMLDivElement, QueueItemProps>(
|
||||
({ status = 'pending', className, children, ...props }, ref) => {
|
||||
const value = React.useMemo(() => ({ status }), [status]);
|
||||
|
||||
return (
|
||||
<QueueItemContext.Provider value={value}>
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="queue-item"
|
||||
data-status={status}
|
||||
role="listitem"
|
||||
className={cn('flex items-start gap-2 py-0.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</QueueItemContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
QueueItem.displayName = 'QueueItem';
|
||||
|
||||
/* ─── QueueItemIndicator ─────────────────────────────────────────── */
|
||||
|
||||
export const QueueItemIndicator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { status } = useQueueItem();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="queue-item-indicator"
|
||||
aria-hidden="true"
|
||||
className={cn('mt-0.5 flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{status === 'completed' && (
|
||||
<svg className="h-3.5 w-3.5 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{status === 'in_progress' && (
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-blue-500 dark:bg-blue-400" />
|
||||
)}
|
||||
{status === 'pending' && (
|
||||
<svg className="h-3.5 w-3.5 text-muted-foreground/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9" strokeWidth={2} />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
QueueItemIndicator.displayName = 'QueueItemIndicator';
|
||||
|
||||
/* ─── QueueItemContent ───────────────────────────────────────────── */
|
||||
|
||||
export const QueueItemContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { status } = useQueueItem();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="queue-item-content"
|
||||
className={cn(
|
||||
'min-w-0 flex-1 text-xs',
|
||||
status === 'completed' && 'text-muted-foreground line-through',
|
||||
status === 'in_progress' && 'font-medium text-foreground',
|
||||
status === 'pending' && 'text-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
QueueItemContent.displayName = 'QueueItemContent';
|
||||
@@ -14,3 +14,5 @@ export { Shimmer } from './Shimmer';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export { PromptInput, PromptInputHeader, PromptInputBody, PromptInputTextarea, PromptInputFooter, PromptInputTools, PromptInputButton, PromptInputSubmit } from './PromptInput';
|
||||
export { PillBar, Pill } from './PillBar';
|
||||
export { Queue, QueueItem, QueueItemIndicator, QueueItemContent } from './Queue';
|
||||
export type { QueueItemStatus } from './Queue';
|
||||
|
||||
Reference in New Issue
Block a user