TaskCommandBar.tsx32.9 KBView on GitHub
'use client';

import type { HydratedUserTask, TaskDateKey } from '@/modules/userTasks/slice/userTasksSlice';
import { generateSimpleDateSuggestions, type DateSuggestion } from '@/modules/crm/utils/time';
import Container3D from '@/modules/cedar-os/src/cedar-os-components/containers/Container3D';
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Calendar, Plus, Building2, User } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { format, isToday, isTomorrow } from 'date-fns';
import { EnterKey } from '@/components/ui/enter-key';
import { useTRPC } from '@/providers/query-provider';
import { useCedarStore } from '@/modules/store';
import useMeasure from 'react-use-measure';
import { v4 as uuidv4 } from 'uuid';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';

/**
 * Get the date key for a given date relative to today
 * Matches the logic in use-tasks-view.ts
 */
function getDateKeyForDate(date: Date): TaskDateKey {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const dateStart = new Date(date);
  dateStart.setHours(0, 0, 0, 0);

  const diffDays = Math.round((dateStart.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));

  if (diffDays < -3) return 'previous';
  if (diffDays === -3) return '-3';
  if (diffDays === -2) return '-2';
  if (diffDays === -1) return '-1';
  if (diffDays === 0) return 'today';
  if (diffDays === 1) return '+1';
  if (diffDays === 2) return '+2';
  if (diffDays === 3) return '+3';
  return 'future';
}

interface TaskCommandBarProps {
  className?: string;
  /**
   * When provided, the command bar will be open by default with this conversation prefilled.
   * After entering description, it will skip directly to the date step.
   */
  prefilledConversation?: {
    id: string;
    name: string;
    companyName: string | null;
  };
  /**
   * Callback when the command bar is closed
   */
  onClose?: () => void;
  /**
   * Whether to render as an inline component (no floating button)
   */
  inline?: boolean;
}

/**
 * Format date for display in the command bar
 */
function formatDateDisplay(date: Date): string {
  if (isToday(date)) {
    return 'Today';
  }
  if (isTomorrow(date)) {
    return 'Tomorrow';
  }
  return format(date, 'EEE, MMM d');
}

/**
 * Conversation search result type
 */
interface ConversationResult {
  id: string;
  name: string;
  companyName: string | null;
}

/**
 * Selected conversation for task
 */
interface SelectedConversation {
  id: string;
  name: string;
  companyName: string | null;
}

/**
 * Command bar step type
 */
type CommandBarStep = 'description' | 'conversation' | 'date';

export function TaskCommandBar({
  className,
  prefilledConversation,
  onClose: onCloseProp,
  inline = false,
}: TaskCommandBarProps) {
  const trpc = useTRPC();
  const queryClient = useQueryClient();

  // State - open by default if prefilled conversation is provided
  const [isOpen, setIsOpen] = useState(!!prefilledConversation);
  const [currentStep, setCurrentStep] = useState<CommandBarStep>('description');
  const [description, setDescription] = useState('');
  const [conversationInput, setConversationInput] = useState(prefilledConversation?.name || '');
  const [selectedConversation, setSelectedConversation] = useState<SelectedConversation | null>(
    prefilledConversation || null,
  );
  const [selectedConversationIndex, setSelectedConversationIndex] = useState(0);
  const [dateInput, setDateInput] = useState('');
  const [selectedDate, setSelectedDate] = useState<Date>(new Date());
  const [suggestions, setSuggestions] = useState<DateSuggestion[]>([]);
  const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);

  // Track if we should skip conversation step (when prefilled)
  const skipConversationStep = !!prefilledConversation;

  // Refs
  const descriptionInputRef = useRef<HTMLInputElement>(null);
  const conversationInputRef = useRef<HTMLInputElement>(null);
  const dateInputRef = useRef<HTMLInputElement>(null);

  // Measure refs for smooth height animations
  const [conversationMeasureRef, conversationBounds] = useMeasure();
  const [dateMeasureRef, dateBounds] = useMeasure();

  // Store actions
  const setTasks = useCedarStore((state) => state.setTasks);
  const removeTask = useCedarStore((state) => state.removeTask);
  const taskIdsByDateKey=[redacted] => state.taskIdsByDateKey);
  const setTaskIdsByDateKey=[redacted] => state.setTaskIdsByDateKey);

  // Track optimistic task ID and date key for cleanup
  const optimisticTaskIdRef = useRef<string | null>(null);
  const optimisticDateKeyRef = useRef<TaskDateKey | null>(null);

  // Debounced conversation search query
  const [debouncedConversationQuery, setDebouncedConversationQuery] = useState('');

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedConversationQuery(conversationInput);
    }, 200);
    return () => clearTimeout(timer);
  }, [conversationInput]);

  // Which deal the task belongs to. `dealsOnly` for the same reason NewTaskDialog passes it: an
  // unfiltered search answers with every auto-created correspondent conversation, which is not
  // somewhere a task is ever filed.
  const conversationSearchQuery = useQuery(
    trpc.crm.searchConversationsMinimal.queryOptions(
      { query: debouncedConversationQuery, limit: 10, dealsOnly: true },
      {
        enabled: currentStep === 'conversation' && debouncedConversationQuery.length >= 1,
        staleTime: 30000, // Cache for 30 seconds
      },
    ),
  );

  const conversationResults = useMemo(() => {
    return conversationSearchQuery.data?.conversations || [];
  }, [conversationSearchQuery.data?.conversations]);

  // Reset selected index when results change
  useEffect(() => {
    setSelectedConversationIndex(0);
  }, [conversationResults]);

  /**
   * Add a task ID to the appropriate date bucket
   */
  const addTaskToDateBucket = useCallback(
    (taskId: string, dueDate: Date) => {
      const dateKey=[redacted];
      optimisticDateKeyRef.current = dateKey;

      // Get current bucket and add the new task ID
      const currentBucket = taskIdsByDateKey[dateKey] || [];
      if (!currentBucket.includes(taskId)) {
        setTaskIdsByDateKey({
          ...taskIdsByDateKey,
          [dateKey]: [...currentBucket, taskId],
        });
      }
    },
    [taskIdsByDateKey, setTaskIdsByDateKey],
  );

  /**
   * Remove a task ID from a date bucket
   */
  const removeTaskFromDateBucket = useCallback(
    (taskId: string, dateKey=[redacted] => {
      const currentBucket = taskIdsByDateKey[dateKey] || [];
      const updatedBucket = currentBucket.filter((id) => id !== taskId);
      setTaskIdsByDateKey({
        ...taskIdsByDateKey,
        [dateKey]: updatedBucket,
      });
    },
    [taskIdsByDateKey, setTaskIdsByDateKey],
  );

  // Create task mutation with optimistic updates
  const createTaskMutation = useMutation(
    trpc.userTasks.createStandaloneTask.mutationOptions({
      onSuccess: (data) => {
        // Remove the optimistic task from store and date bucket
        if (optimisticTaskIdRef.current && optimisticDateKeyRef.current) {
          removeTask(optimisticTaskIdRef.current);
          removeTaskFromDateBucket(optimisticTaskIdRef.current, optimisticDateKeyRef.current);
        }

        // Add the real task from server
        if (data.task) {
          const realTask: HydratedUserTask = {
            ...data.task,
          } as HydratedUserTask;

          // Add to tasks store
          setTasks({ [data.task.id]: realTask });

          // Add to the correct date bucket
          addTaskToDateBucket(data.task.id, realTask.dueDate);
        }

        // Clear refs
        optimisticTaskIdRef.current = null;
        optimisticDateKeyRef.current = null;

        // Invalidate queries to refetch and sync
        queryClient.invalidateQueries({ queryKey: [['userTasks']] });
      },
      onError: (error) => {
        toast.error('Failed to create task');
        console.error('Failed to create task:', error);

        // Remove the optimistic task on error
        if (optimisticTaskIdRef.current && optimisticDateKeyRef.current) {
          removeTask(optimisticTaskIdRef.current);
          removeTaskFromDateBucket(optimisticTaskIdRef.current, optimisticDateKeyRef.current);
        }

        // Clear refs
        optimisticTaskIdRef.current = null;
        optimisticDateKeyRef.current = null;
      },
    }),
  );

  // Helper to normalize dates to 00:01 so "Today" shows up in current tasks
  const normalizeDateSuggestions = useCallback((rawSuggestions: DateSuggestion[]) => {
    return rawSuggestions.map((suggestion) => {
      const normalizedDate = new Date(suggestion.date);
      normalizedDate.setHours(0, 1, 0, 0); // 00:01:00.000
      return { ...suggestion, date: normalizedDate };
    });
  }, []);

  // Update date suggestions when input changes or when expanded
  useEffect(() => {
    if (currentStep === 'date') {
      const rawSuggestions = generateSimpleDateSuggestions(dateInput);
      const newSuggestions = normalizeDateSuggestions(rawSuggestions);
      setSuggestions(newSuggestions);
      setSelectedSuggestionIndex(0);

      // Auto-select first parsed date if available
      if (newSuggestions.length > 0 && dateInput.trim()) {
        setSelectedDate(newSuggestions[0].date);
      }
    }
  }, [dateInput, currentStep, normalizeDateSuggestions]);

  // Initialize suggestions when entering date step
  useEffect(() => {
    if (currentStep === 'date') {
      setSuggestions(normalizeDateSuggestions(generateSimpleDateSuggestions('')));
    }
  }, [currentStep, normalizeDateSuggestions]);

  // Define handleClose before useEffect that depends on it
  const handleClose = useCallback(() => {
    setIsOpen(false);
    setCurrentStep('description');
    setDescription('');
    setConversationInput(prefilledConversation?.name || '');
    setSelectedConversation(prefilledConversation || null);
    setDateInput('');
    setSelectedDate(new Date());
    onCloseProp?.();
  }, [prefilledConversation, onCloseProp]);

  // Handle Cmd+D to open/focus
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
        // Don't intercept if user is typing in a TipTap/contenteditable editor
        const activeElement = document.activeElement;
        const isInEditor =
          activeElement?.closest('.ProseMirror') ||
          activeElement?.getAttribute('contenteditable') === 'true';

        if (isInEditor) {
          return; // Let the editor handle Cmd+K
        }

        e.preventDefault();
        e.stopPropagation();

        if (!isOpen) {
          setIsOpen(true);
        } else {
          descriptionInputRef.current?.focus();
        }
      }

      // Escape to close or go back
      if (e.key === 'Escape' && isOpen) {
        e.preventDefault();
        if (currentStep === 'date') {
          // Go back to conversation step
          setCurrentStep('conversation');
          setDateInput('');
          setTimeout(() => conversationInputRef.current?.focus(), 100);
        } else if (currentStep === 'conversation') {
          // Go back to description step
          setCurrentStep('description');
          setConversationInput('');
          setSelectedConversation(null);
          setTimeout(() => descriptionInputRef.current?.focus(), 100);
        } else {
          handleClose();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown, { capture: true });
    return () => {
      window.removeEventListener('keydown', handleKeyDown, { capture: true });
    };
  }, [isOpen, currentStep, handleClose]);

  // Focus input when step changes
  useEffect(() => {
    if (isOpen) {
      setTimeout(() => {
        if (currentStep === 'description') {
          descriptionInputRef.current?.focus();
        } else if (currentStep === 'conversation') {
          conversationInputRef.current?.focus();
        } else if (currentStep === 'date') {
          dateInputRef.current?.focus();
        }
      }, 100);
    }
  }, [isOpen, currentStep]);

  const handleDescriptionKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter' && description.trim()) {
        e.preventDefault();
        // Skip to date step if conversation is prefilled, otherwise go to conversation step
        if (skipConversationStep) {
          setCurrentStep('date');
        } else {
          setCurrentStep('conversation');
        }
      }
    },
    [description, skipConversationStep],
  );

  const handleConversationSelect = useCallback((conversation: ConversationResult) => {
    setSelectedConversation(conversation);
    setConversationInput(conversation.name);
    // Move to date step
    setCurrentStep('date');
  }, []);

  const handleSkipConversation = useCallback(() => {
    setSelectedConversation(null);
    setCurrentStep('date');
  }, []);

  const handleConversationKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        if (conversationResults.length > 0) {
          // Select the highlighted conversation
          handleConversationSelect(conversationResults[selectedConversationIndex]);
        } else if (conversationInput.trim() === '') {
          // Skip if empty
          handleSkipConversation();
        }
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        setSelectedConversationIndex((prev) => Math.min(prev + 1, conversationResults.length - 1));
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setSelectedConversationIndex((prev) => Math.max(prev - 1, 0));
      } else if (e.key === 'Backspace' && !conversationInput) {
        // Go back to description input
        e.preventDefault();
        setCurrentStep('description');
        setTimeout(() => descriptionInputRef.current?.focus(), 100);
      } else if (e.key === 'Tab' && !e.shiftKey) {
        // Tab to skip conversation and go to date
        e.preventDefault();
        handleSkipConversation();
      }
    },
    [
      conversationResults,
      selectedConversationIndex,
      conversationInput,
      handleConversationSelect,
      handleSkipConversation,
    ],
  );

  const handleCreateTask = useCallback(
    (overrideDate?: Date) => {
      if (!description.trim()) {
        toast.error('Please enter a task description');
        return;
      }

      // Use override date if provided (for immediate selection), otherwise use state
      const taskDueDate = overrideDate || selectedDate;

      // Create optimistic task with a proper structure (using new schema fields)
      const optimisticTaskId = uuidv4();
      const now = new Date();
      const optimisticTask: HydratedUserTask = {
        id: optimisticTaskId,
        userId: 'optimistic', // Placeholder - will be replaced by server
        conversationId: selectedConversation?.id || 'optimistic', // Use selected or placeholder
        creationRunId: null, // Reference to execution that CREATED this task
        executionRunId: null, // Reference to execution that EXECUTED this task
        // A user typed this task; it did not come from a note marker.
        sourceDocumentId: null,
        sourceMarkerId: null,

        // Status
        status: 'todo',
        // Obligation tags are agent-written at create time; a user-created task carries none.
        tags: [],
        isRead: true, // Mark as read since user just created it

        // Core fields
        description: description.trim(),
        notes: null,
        dueDate: taskDueDate,

        // New schema fields
        taskChannel: 'email', // Default channel for standalone tasks
        taskType: 'manual',
        taskCreatedBy: 'user', // User-created task
        taskActionData: null,
        // Output axis — bare intent; nothing is produced until the task is worked.
        taskOutput: { kind: 'email' },

        // Agent execution
        agentExecutionEnabled: false,

        // Timestamps
        createdAt: now,
        updatedAt: now,
        completedAt: null,

        // Board position. The server places a new task by its due date (see
        // user_tasks_sort_order.sql); 0 is a placeholder that lives only until the real row
        // arrives, and never pins — the user has not dragged this anywhere.
        sortOrder: 0,
        sortOrderPinned: false,

        // Hydrated fields
        taskGroupId: null,
        chatThreadId: null,
        // The email thread the task is about — an optimistic row created from the
        // command bar has no thread in hand.
        sourceThreadId: null,
        conversation: selectedConversation ? { name: selectedConversation.name } : null,
      };

      // Store the optimistic task ID for cleanup in mutation callbacks
      optimisticTaskIdRef.current = optimisticTaskId;

      // Add optimistic task to the tasks store
      setTasks({ [optimisticTaskId]: optimisticTask });

      // Add to the appropriate date bucket for immediate UI update
      addTaskToDateBucket(optimisticTaskId, taskDueDate);

      // Call mutation
      createTaskMutation.mutate({
        description: description.trim(),
        dueDate: taskDueDate.toISOString(),
        conversationId: selectedConversation?.id,
      });

      // Close and reset
      handleClose();
    },
    [
      description,
      selectedDate,
      selectedConversation,
      setTasks,
      addTaskToDateBucket,
      createTaskMutation,
      handleClose,
    ],
  );

  const handleDateKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        // Create the task with currently selected date
        handleCreateTask(selectedDate);
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        setSelectedSuggestionIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
        if (suggestions[Math.min(selectedSuggestionIndex + 1, suggestions.length - 1)]) {
          setSelectedDate(
            suggestions[Math.min(selectedSuggestionIndex + 1, suggestions.length - 1)].date,
          );
        }
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setSelectedSuggestionIndex((prev) => Math.max(prev - 1, 0));
        if (suggestions[Math.max(selectedSuggestionIndex - 1, 0)]) {
          setSelectedDate(suggestions[Math.max(selectedSuggestionIndex - 1, 0)].date);
        }
      } else if (e.key === 'Backspace' && !dateInput) {
        // Go back to conversation input (or description if skipping conversation)
        e.preventDefault();
        if (skipConversationStep) {
          setCurrentStep('description');
          setTimeout(() => descriptionInputRef.current?.focus(), 100);
        } else {
          setCurrentStep('conversation');
          setTimeout(() => conversationInputRef.current?.focus(), 100);
        }
      }
    },
    [
      suggestions,
      selectedSuggestionIndex,
      dateInput,
      selectedDate,
      handleCreateTask,
      skipConversationStep,
    ],
  );

  const handleSuggestionSelect = useCallback(
    (suggestion: DateSuggestion) => {
      // Create task immediately with the selected date
      handleCreateTask(suggestion.date);
    },
    [handleCreateTask],
  );

  const handleConversationButtonClick = useCallback(() => {
    // Allow clicking to switch to conversation step from any step (not just description)
    if (description.trim()) {
      setCurrentStep('conversation');
    }
  }, [description]);

  const handleDescriptionButtonClick = useCallback(() => {
    // Allow clicking to switch back to description step from any step
    setCurrentStep('description');
  }, []);

  const handleDateSectionClick = useCallback(() => {
    // Allow clicking to switch to date step if description is filled
    if (description.trim()) {
      setCurrentStep('date');
    }
  }, [description]);

  // Don't render if not open
  if (!isOpen) {
    // If inline mode, don't render anything when closed
    if (inline) {
      return null;
    }
    return (
      <div className={cn('absolute bottom-4 left-1/2 z-50 -translate-x-1/2', className)}>
        <motion.button
          layoutId="task-command-bar-container"
          onClick={() => setIsOpen(true)}
          className="bg-background/80 hover:bg-background border-border flex items-center gap-2 rounded-full border px-4 py-2 text-sm shadow-lg backdrop-blur-sm transition-all hover:shadow-xl"
          whileHover={{ scale: 1.02 }}
          whileTap={{ scale: 0.98 }}
        >
          <Plus className="h-4 w-4" />
          <span>Add task</span>
          <kbd className="bg-muted text-muted-foreground ml-2 rounded px-1.5 py-0.5 text-xs">
            ⌘D
          </kbd>
        </motion.button>
      </div>
    );
  }

  const isDescriptionStep = currentStep === 'description';

  const isConversationExpanded = currentStep === 'conversation';
  const isDateExpanded = currentStep === 'date';

  return (
    <Container3D
      className={cn(
        'w-full overflow-hidden shadow-xl',
        inline
          ? 'relative max-w-full'
          : 'absolute bottom-4 left-1/2 z-50 max-w-lg -translate-x-1/2',
        className,
      )}
      motionProps={{
        layoutId: inline ? undefined : 'task-command-bar-container',
        transition: {
          type: 'spring',
          stiffness: 400,
          damping: 30,
        },
      }}
    >
      <Command className="rounded-xl" shouldFilter={false}>
        {/* Conversation suggestions - shown when in conversation step */}
        <motion.div
          initial={false}
          animate={{
            height: isConversationExpanded ? conversationBounds.height : 0,
            opacity: isConversationExpanded ? 1 : 0,
          }}
          transition={{ duration: 0.2, ease: 'easeOut' }}
          className="overflow-hidden"
        >
          <div ref={conversationMeasureRef}>
            <CommandList className="max-h-[200px] overflow-y-auto">
              <CommandGroup
                heading={
                  conversationInput.trim()
                    ? 'Matching conversations'
                    : 'Link to a conversation (optional)'
                }
              >
                {/* Skip option */}
                {conversationInput.trim() === '' && (
                  <CommandItem
                    value="skip"
                    onSelect={handleSkipConversation}
                    className="text-muted-foreground flex cursor-pointer items-center gap-2 italic"
                  >
                    <User className="h-4 w-4" />
                    <span className="flex-1 text-sm">Personal task (no conversation)</span>
                    <span className="text-xs">Tab to skip</span>
                  </CommandItem>
                )}
                {/* Loading state */}
                {conversationSearchQuery.isLoading && debouncedConversationQuery && (
                  <CommandItem disabled className="text-muted-foreground">
                    <span className="text-sm">Searching...</span>
                  </CommandItem>
                )}
                {/* Results */}
                {conversationResults.map((conversation: ConversationResult, index: number) => (
                  <CommandItem
                    key=[redacted]
                    value={conversation.id}
                    onSelect={() => handleConversationSelect(conversation)}
                    className={cn(
                      'flex cursor-pointer items-center gap-2',
                      index === selectedConversationIndex && 'bg-accent',
                    )}
                  >
                    <Building2 className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                    <div className="flex min-w-0 flex-1 flex-col">
                      <span className="truncate text-sm">{conversation.name}</span>
                      {conversation.companyName && (
                        <span className="text-muted-foreground truncate text-xs">
                          {conversation.companyName}
                        </span>
                      )}
                    </div>
                  </CommandItem>
                ))}
                {/* No results */}
                {!conversationSearchQuery.isLoading &&
                  conversationResults.length === 0 &&
                  debouncedConversationQuery && (
                    <CommandItem
                      value="no-results"
                      onSelect={handleSkipConversation}
                      className="text-muted-foreground flex cursor-pointer items-center gap-2"
                    >
                      <User className="h-4 w-4" />
                      <span className="text-sm">
                        No conversations found. Press Enter to continue without linking.
                      </span>
                    </CommandItem>
                  )}
              </CommandGroup>
            </CommandList>
          </div>
        </motion.div>

        {/* Date suggestions - shown when in date step */}
        <motion.div
          initial={false}
          animate={{
            height: isDateExpanded ? dateBounds.height : 0,
            opacity: isDateExpanded ? 1 : 0,
          }}
          transition={{ duration: 0.2, ease: 'easeOut' }}
          className="overflow-hidden"
        >
          <div ref={dateMeasureRef}>
            <CommandList className="max-h-[200px] overflow-y-auto">
              <CommandGroup heading="When is this due?">
                {suggestions.map((suggestion, index) => (
                  <CommandItem
                    key=[redacted]
                    value={suggestion.id}
                    onSelect={() => handleSuggestionSelect(suggestion)}
                    className={cn(
                      'flex cursor-pointer items-center gap-2',
                      index === selectedSuggestionIndex && 'bg-accent',
                    )}
                  >
                    <Calendar className="text-muted-foreground h-4 w-4" />
                    <span className="flex-1 text-sm">{suggestion.label}</span>
                    {suggestion.shortLabel && (
                      <span className="text-muted-foreground text-xs">{suggestion.shortLabel}</span>
                    )}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandList>
          </div>
        </motion.div>

        {/* Main input row - always visible */}
        <div
          className={cn(
            'p-3',
            (isConversationExpanded || isDateExpanded) && 'border-border border-t',
          )}
        >
          <div className="flex items-center gap-2">
            {/* Description input */}
            <motion.div
              layout
              layoutId="task-description-section"
              className="flex min-w-0 flex-1 items-center gap-2"
            >
              {isDescriptionStep ? (
                <>
                  <motion.div layoutId="task-plus-icon">
                    <Plus className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                  </motion.div>
                  <input
                    ref={descriptionInputRef}
                    type="text"
                    value={description}
                    onChange={(e) => setDescription(e.target.value)}
                    onKeyDown={handleDescriptionKeyDown}
                    placeholder="What do you need to do?"
                    className="placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-sm outline-none"
                    autoFocus
                  />
                  {/* Show enter hint when description has content */}
                  <AnimatePresence>
                    {description.trim() && (
                      <motion.kbd
                        initial={{ opacity: 0, filter: 'blur(4px)' }}
                        animate={{ opacity: 1, filter: 'blur(0px)' }}
                        exit={{ opacity: 0, filter: 'blur(4px)' }}
                        transition={{ duration: 0.2 }}
                        className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs"
                      >
                        <EnterKey />
                      </motion.kbd>
                    )}
                  </AnimatePresence>
                </>
              ) : (
                <button
                  onClick={handleDescriptionButtonClick}
                  className="hover:bg-accent flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-2 py-1 transition-colors"
                >
                  <motion.div layoutId="task-plus-icon">
                    <Plus className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                  </motion.div>
                  <span className="min-w-0 flex-1 truncate text-left text-sm">
                    {description || 'What do you need to do?'}
                  </span>
                </button>
              )}
            </motion.div>

            {/* Separator */}
            <motion.div layoutId="task-separator-1" className="bg-border h-5 w-px flex-shrink-0" />

            {/* Conversation section */}
            <motion.div
              layout
              layoutId="task-conversation-section"
              className="flex flex-shrink-0 items-center gap-2"
            >
              {isConversationExpanded ? (
                <>
                  <motion.div layoutId="task-building-icon">
                    <Building2 className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                  </motion.div>
                  <input
                    ref={conversationInputRef}
                    type="text"
                    value={conversationInput}
                    onChange={(e) => setConversationInput(e.target.value)}
                    onKeyDown={handleConversationKeyDown}
                    placeholder="Search a deal…"
                    className="w-36 bg-transparent text-sm outline-none"
                    autoFocus
                  />
                  <kbd className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs">
                    <EnterKey />
                  </kbd>
                </>
              ) : (
                <button
                  onClick={handleConversationButtonClick}
                  disabled={!description.trim()}
                  className={cn(
                    'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
                    description.trim()
                      ? 'hover:bg-accent cursor-pointer'
                      : 'cursor-not-allowed opacity-50',
                  )}
                >
                  <motion.div layoutId="task-building-icon">
                    <Building2 className="text-muted-foreground h-4 w-4" />
                  </motion.div>
                  <span className="text-muted-foreground max-w-24 truncate">
                    {selectedConversation ? selectedConversation.name : 'Personal'}
                  </span>
                </button>
              )}
            </motion.div>

            {/* Separator */}
            <motion.div layoutId="task-separator-2" className="bg-border h-5 w-px flex-shrink-0" />

            {/* Date section */}
            <motion.div
              layout
              layoutId="task-date-section"
              className="flex flex-shrink-0 items-center gap-2"
            >
              {isDateExpanded ? (
                <>
                  <motion.div layoutId="task-calendar-icon">
                    <Calendar className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                  </motion.div>
                  <input
                    ref={dateInputRef}
                    type="text"
                    value={dateInput}
                    onChange={(e) => setDateInput(e.target.value)}
                    onKeyDown={handleDateKeyDown}
                    placeholder={'next tuesday at 9:30am'}
                    className="w-40 bg-transparent text-sm outline-none"
                    autoFocus
                  />
                  <kbd className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs">
                    <EnterKey />
                  </kbd>
                </>
              ) : (
                <button
                  onClick={handleDateSectionClick}
                  disabled={!description.trim()}
                  className={cn(
                    'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
                    description.trim()
                      ? 'hover:bg-accent cursor-pointer'
                      : 'cursor-not-allowed opacity-50',
                  )}
                >
                  <motion.div layoutId="task-calendar-icon">
                    <Calendar className="text-muted-foreground h-4 w-4" />
                  </motion.div>
                  <span className="text-muted-foreground">{formatDateDisplay(selectedDate)}</span>
                </button>
              )}
            </motion.div>
          </div>
        </div>
      </Command>
    </Container3D>
  );
}