Ctrl K
ComunidadGuías…

components/tool-todo-display.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import {
  IconCheck as Check,
  IconListCheck as ListTodo
} from '@tabler/icons-react'

import { toPublicErrorPayload } from '@/lib/errors/public-error'
import { Part, TodoItem } from '@/lib/types/ai'
import { cn } from '@/lib/utils'

import { useArtifact } from './artifact/artifact-context'
import { CollapsibleMessage } from './collapsible-message'
import ProcessHeader from './process-header'
import TodoListContent from './todo-list-content'

interface ToolTodoDisplayProps {
  tool: 'todoWrite'
  state:
    | 'input-streaming'
    | 'input-available'
    | 'output-available'
    | 'output-error'
  input?: {
    todos?: TodoItem[]
  }
  output?: {
    todos?: TodoItem[]
    message?: string
    completedCount?: number
    totalCount?: number
  }
  errorText?: string
  toolCallId: string
  isOpen?: boolean
  onOpenChange?: (open: boolean) => void
  borderless?: boolean
  isFirst?: boolean
  isLast?: boolean
}

export function ToolTodoDisplay({
  tool,
  state,
  output,
  errorText,
  toolCallId,
  isOpen = false,
  onOpenChange,
  borderless = false,
  isFirst = false,
  isLast = false
}: ToolTodoDisplayProps) {
  const { open: openArtifact } = useArtifact()
  // Calculate counts for display
  const completedCount =
    output?.completedCount ??
    (output?.todos
      ? output.todos.filter(t => t.status === 'completed').length
      : 0)
  const totalCount =
    output?.totalCount ?? (output?.todos ? output.todos.length : 0)
  const errorMessage =
    state === 'output-error'
      ? toPublicErrorPayload(errorText ?? output?.message, {
          fallbackMessage: 'Todo tool failed'
        }).error
      : undefined

  const isLoading = state === 'input-streaming' || state === 'input-available'

  const openInspector = () => {
    if (!(state === 'output-available' && output)) return
    const part: Part = {
      type: 'tool-todoWrite',
      toolCallId,
      state,
      input: { todos: output.todos || [] },
      output
    }
    openArtifact(part)
  }

  const header = (
    <ProcessHeader
      onInspect={openInspector}
      isLoading={isLoading}
      label={
        <span className="inline-flex items-center gap-2 min-w-0 overflow-hidden">
          <ListTodo className="size-4 text-muted-foreground shrink-0" />
          <span className="truncate">
            {state === 'output-available' && output
              ? output.message || 'Updated tasks'
              : 'Updating tasks...'}
          </span>
        </span>
      }
      meta={
        state === 'output-available' && totalCount > 0 ? (
          <span className="flex items-center gap-1 text-muted-foreground">
            {completedCount === totalCount ? (
              <Check className="size-4 text-green-500" />
            ) : null}
            <span className="text-xs">
              ({completedCount}/{totalCount})
            </span>
          </span>
        ) : undefined
      }
      ariaExpanded={isOpen}
    />
  )

  return (
    <div className="relative">
      {/* Rails for header - show based on position */}
      {borderless && (
        <>
          {!isFirst && (
            <div className="absolute left-[19.5px] w-px bg-border h-2 top-0" />
          )}
          {!isLast && (
            <div className="absolute left-[19.5px] w-px bg-border h-2 bottom-0" />
          )}
        </>
      )}
      <CollapsibleMessage
        role="assistant"
        isCollapsible={true}
        header={header}
        isOpen={isOpen}
        onOpenChange={onOpenChange}
        showBorder={!borderless}
        showIcon={false}
        variant="default"
        showSeparator={false}
        headerClickBehavior="split"
      >
        <div className="flex">
          {/* Rail space - always reserved when grouped */}
          {borderless && (
            <>
              <div className="w-[16px] shrink-0 flex justify-center">
                <div
                  className={cn(
                    'w-px bg-border/50 transition-opacity duration-200',
                    isOpen ? 'opacity-100' : 'opacity-0'
                  )}
                  style={{
                    marginTop: isFirst ? '0' : '-1rem',
                    marginBottom: isLast ? '0' : '-1rem'
                  }}
                />
              </div>
              <div className="w-2 shrink-0" />
            </>
          )}
          <div className="flex-1">
            {state === 'output-available' ? (
              <TodoListContent
                todos={output?.todos}
                message={output?.message}
                completedCount={completedCount}
                totalCount={totalCount}
                showSummary={false}
                itemVariant="plain"
                className="pb-1"
              />
            ) : state === 'output-error' ? (
              <div className="px-3 pb-3 text-xs text-destructive">
                {errorMessage}
              </div>
            ) : null}
          </div>
        </div>
      </CollapsibleMessage>
    </div>
  )
}