"use client"; import { forwardRef, useCallback, useState } from "react"; import { IconAlertTriangle, IconPlus, IconPlayerPlay } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { NewSessionDialog } from "@/components/task/new-session-dialog"; import { useAppStore } from "@/components/state-provider"; import type { ContextFile } from "@/lib/state/context-files-store"; import type { Message } from "@/lib/types/http"; import type { DiffComment } from "@/lib/diff/types"; import { getWebSocketClient } from "@/lib/ws/connection"; import { useChatInputContainer } from "./use-chat-input-container"; import { ChatInputBody, type ChatInputContextAreaProps, type ChatInputEditorAreaProps, } from "./chat-input-body"; import type { ContextItem } from "@/lib/types/context"; import { useUtilityAgentGenerator } from "@/hooks/use-utility-agent-generator"; import { useIsUtilityConfigured } from "@/hooks/use-is-utility-configured"; // Re-export ImageAttachment type for consumers export type { ImageAttachment } from "./image-attachment-preview"; // Type for message attachments sent to backend export type MessageAttachment = { type: "image" | "audio" | "resource"; data: string; mime_type: string; name?: string; }; export type ChatInputContainerHandle = { focusInput: () => void; getTextareaElement: () => HTMLElement | null; getValue: () => string; getSelectionStart: () => number; insertText: (text: string, from: number, to: number) => void; clear: () => void; }; type ChatInputContainerProps = { onSubmit: ( message: string, reviewComments?: DiffComment[], attachments?: MessageAttachment[], inlineMentions?: ContextFile[], ) => void; sessionId: string | null; taskId: string | null; taskTitle?: string; taskDescription: string; planModeEnabled: boolean; planModeAvailable?: boolean; mcpServers?: string[]; onPlanModeChange: (enabled: boolean) => void; isAgentBusy: boolean; isStarting: boolean; isMoving?: boolean; isSending: boolean; onCancel: () => void; placeholder?: string; pendingClarification?: Message | null; onClarificationResolved?: () => void; showRequestChangesTooltip?: boolean; onRequestChangesTooltipDismiss?: () => void; pendingCommentsByFile?: Record; hasContextComments?: boolean; submitKey?: "enter" | "cmd_enter"; hasAgentCommands?: boolean; isFailed?: boolean; needsRecovery?: boolean; contextItems?: ContextItem[]; planContextEnabled?: boolean; contextFiles?: ContextFile[]; onToggleContextFile?: (file: ContextFile) => void; onAddContextFile?: (file: ContextFile) => void; onImplementPlan?: () => void; hideSessionsDropdown?: boolean; minimalToolbar?: boolean; /** Hide the plan mode toggle button (for ephemeral/quick chat sessions) */ hidePlanMode?: boolean; }; function FailedSessionBanner({ showDialog, onShowDialog, taskId, sessionId, }: { showDialog: boolean; onShowDialog: (open: boolean) => void; taskId: string | null; sessionId: string | null; }) { const [isResuming, setIsResuming] = useState(false); const agentProfileId = useAppStore((s) => sessionId ? (s.taskSessions.items[sessionId]?.agent_profile_id ?? "") : "", ); const profileExists = useAppStore( (s) => agentProfileId !== "" && s.agentProfiles.items.some((p: { id: string }) => p.id === agentProfileId), ); const handleResume = useCallback(async () => { if (!sessionId || !taskId) return; const client = getWebSocketClient(); if (!client) return; setIsResuming(true); try { await client.request( "session.launch", { task_id: taskId, intent: "resume", session_id: sessionId }, 30000, ); } catch { setIsResuming(false); } }, [sessionId, taskId]); return ( <>
This agent has stopped.
{sessionId && taskId && ( {!profileExists && Agent profile no longer exists} )}
{taskId && } ); } type ContainerState = ReturnType; function buildContextAreaProps( s: ContainerState, p: ChatInputContainerProps, ): ChatInputContextAreaProps { return { hasContextZone: s.hasContextZone, allItems: s.allItems, sessionId: p.sessionId, }; } type EnhancePromptExtras = { onEnhancePrompt?: () => void; isEnhancingPrompt?: boolean; isUtilityConfigured?: boolean; }; function buildEditorAreaProps( s: ContainerState, p: ChatInputContainerProps, extras: EnhancePromptExtras = {}, ): ChatInputEditorAreaProps { return { inputRef: s.inputRef, value: s.value, handleChange: s.handleChange, handleSubmitWithReset: s.handleSubmitWithReset, inputPlaceholder: s.inputPlaceholder, isDisabled: s.isDisabled, hasClarification: s.hasClarification, planModeEnabled: p.planModeEnabled, planModeAvailable: p.planModeAvailable ?? true, mcpServers: p.mcpServers ?? [], submitKey: p.submitKey ?? "cmd_enter", setIsInputFocused: s.setIsInputFocused, sessionId: p.sessionId, taskId: p.taskId, onAddContextFile: p.onAddContextFile, onToggleContextFile: p.onToggleContextFile, planContextEnabled: p.planContextEnabled ?? false, handleAgentCommand: s.handleAgentCommand, addFiles: s.addFiles, fileInputRef: s.fileInputRef, showRequestChangesTooltip: p.showRequestChangesTooltip ?? false, isAgentBusy: p.isAgentBusy, onPlanModeChange: p.onPlanModeChange, taskTitle: p.taskTitle, taskDescription: p.taskDescription, isSending: p.isSending, onCancel: p.onCancel, contextCount: s.allItems.length, contextPopoverOpen: s.contextPopoverOpen, setContextPopoverOpen: s.setContextPopoverOpen, contextFiles: p.contextFiles ?? [], onImplementPlan: p.onImplementPlan, onEnhancePrompt: extras.onEnhancePrompt, isEnhancingPrompt: extras.isEnhancingPrompt, isUtilityConfigured: extras.isUtilityConfigured, hideSessionsDropdown: p.hideSessionsDropdown, minimalToolbar: p.minimalToolbar, hidePlanMode: p.hidePlanMode, }; } export const ChatInputContainer = forwardRef( function ChatInputContainer(props, ref) { const { sessionId, taskId, taskTitle, taskDescription, isAgentBusy, isStarting, isMoving = false, isSending, isFailed = false, showRequestChangesTooltip = false, } = props; const isBusyVisual = isStarting || isMoving; const p = { ...props, isFailed: isFailed ?? false, hasAgentCommands: props.hasAgentCommands ?? false, submitKey: props.submitKey ?? "cmd_enter", planContextEnabled: props.planContextEnabled ?? false, contextFiles: props.contextFiles ?? [], contextItems: props.contextItems ?? [], showRequestChangesTooltip, } as const; const s = useChatInputContainer({ ref, sessionId, isSending, isStarting, isMoving, isFailed: p.isFailed, needsRecovery: props.needsRecovery ?? false, isAgentBusy, hasAgentCommands: p.hasAgentCommands, placeholder: props.placeholder, contextItems: p.contextItems, pendingClarification: props.pendingClarification, onClarificationResolved: props.onClarificationResolved, pendingCommentsByFile: props.pendingCommentsByFile, hasContextComments: props.hasContextComments ?? false, showRequestChangesTooltip, onRequestChangesTooltipDismiss: props.onRequestChangesTooltipDismiss, onSubmit: props.onSubmit, }); const isUtilityConfigured = useIsUtilityConfigured(); const { enhancePrompt, isEnhancingPrompt } = useUtilityAgentGenerator({ sessionId, taskTitle, taskDescription, }); const handleEnhancePrompt = useCallback(() => { const currentValue = s.value?.trim(); if (!currentValue) return; enhancePrompt(currentValue, (enhanced) => { // Use setValue to directly update TipTap editor (handleChange only updates React state) s.inputRef.current?.setValue(enhanced); }); }, [s, enhancePrompt]); if (p.isFailed) { return ( ); } return ( ); }, );