Files
kandev/apps/web/components/task/chat/chat-input-toolbar.tsx
T
2026-06-27 16:11:39 +01:00

179 lines
6.3 KiB
TypeScript

"use client";
import { memo } from "react";
import { SHORTCUTS } from "@/lib/keyboard/constants";
import { DesktopChatInputToolbar } from "./chat-input-toolbar-desktop";
import { MobileChatInputToolbar } from "./chat-input-toolbar-mobile";
import { SubmitButton } from "./chat-input-toolbar-primitives";
import { shouldUseCompactTaskChrome } from "@/hooks/use-compact-task-chrome";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import type { ContextFile } from "@/lib/state/context-files-store";
export type ChatInputToolbarProps = {
planModeEnabled: boolean;
planModeAvailable?: boolean;
mcpServers?: string[];
onPlanModeChange: (enabled: boolean) => void;
sessionId: string | null;
taskId: string | null;
taskTitle?: string;
taskDescription: string;
isAgentBusy: boolean;
/** Whether the input has content to send (text, comments, or context) */
hasContent?: boolean;
isDisabled: boolean;
submitDisabledReason?: string;
isSending: boolean;
onCancel: () => void | Promise<void>;
onSubmit: () => void;
submitKey?: "enter" | "cmd_enter";
contextCount?: number;
contextPopoverOpen?: boolean;
onContextPopoverOpenChange?: (open: boolean) => void;
/** Whether plan is selected as context in the popover (independent of plan panel) */
planContextEnabled?: boolean;
contextFiles?: ContextFile[];
onToggleFile?: (file: ContextFile) => void;
onImplementPlan?: (fresh: boolean) => void;
/** Callback to enhance the current prompt with AI */
onEnhancePrompt?: () => void;
/** Whether prompt enhancement is in progress */
isEnhancingPrompt?: boolean;
/** Whether utility agent is configured for AI enhancement */
isUtilityConfigured?: boolean;
/** Callback to open file picker for attaching files */
onAttachFiles?: () => void;
/** Callback to insert a transcribed voice utterance into the editor. When
* omitted, the voice button is hidden — keeps quick-chat / read-only
* variants free of a button they can't wire. */
onVoiceTranscript?: (text: string) => void;
/** Optional auto-send hook fired after a voice transcript is inserted. */
onVoiceAutoSend?: () => void;
/** Hide the sessions dropdown (for quick chat) */
hideSessionsDropdown?: boolean;
/** When true, only render the submit/cancel button — no other controls */
minimalToolbar?: boolean;
/** Hide ACP/session-specific controls while keeping Plan, attachment, and context controls */
hideAgentControls?: boolean;
/** Hide the plan mode toggle button (for ephemeral/quick chat sessions) */
hidePlanMode?: boolean;
};
function MinimalToolbar({
isAgentBusy,
hasContent,
isDisabled,
submitDisabledReason,
isSending,
onCancel,
onSubmit,
submitKey = "cmd_enter",
}: Pick<
ChatInputToolbarProps,
| "isAgentBusy"
| "hasContent"
| "isDisabled"
| "submitDisabledReason"
| "isSending"
| "onCancel"
| "onSubmit"
| "submitKey"
>) {
const submitShortcut = submitKey === "enter" ? SHORTCUTS.SUBMIT_ENTER : SHORTCUTS.SUBMIT;
return (
<div className="flex items-center justify-end gap-1 px-1 pt-0 pb-0.5 border-t border-border">
<SubmitButton
isAgentBusy={isAgentBusy}
hasContent={hasContent ?? false}
isDisabled={isDisabled}
submitDisabledReason={submitDisabledReason}
isSending={isSending}
planModeEnabled={false}
onCancel={onCancel}
onSubmit={onSubmit}
submitShortcut={submitShortcut}
/>
</div>
);
}
const toolbarDefaults = {
planModeAvailable: true,
mcpServers: [] as string[],
submitKey: "cmd_enter" as const,
contextCount: 0,
contextPopoverOpen: false,
planContextEnabled: false,
contextFiles: [] as ContextFile[],
isEnhancingPrompt: false,
isUtilityConfigured: false,
hideAgentControls: false,
hidePlanMode: false,
};
export const ChatInputToolbar = memo(function ChatInputToolbar(rawProps: ChatInputToolbarProps) {
const props = { ...toolbarDefaults, ...rawProps };
const submitShortcut = props.submitKey === "enter" ? SHORTCUTS.SUBMIT_ENTER : SHORTCUTS.SUBMIT;
const responsiveBreakpoint = useResponsiveBreakpoint();
const usesCompactTaskChrome = shouldUseCompactTaskChrome(responsiveBreakpoint);
if (props.minimalToolbar) {
return (
<MinimalToolbar
isAgentBusy={props.isAgentBusy}
hasContent={props.hasContent}
isDisabled={props.isDisabled}
submitDisabledReason={props.submitDisabledReason}
isSending={props.isSending}
onCancel={props.onCancel}
onSubmit={props.onSubmit}
submitKey={props.submitKey}
/>
);
}
if (usesCompactTaskChrome) {
return (
<MobileChatInputToolbar
planModeEnabled={props.planModeEnabled}
planModeAvailable={props.planModeAvailable}
onPlanModeChange={props.onPlanModeChange}
hidePlanMode={props.hidePlanMode}
hideAgentControls={props.hideAgentControls}
hideSessionsDropdown={
(props.hideSessionsDropdown ?? false) || responsiveBreakpoint.isMobile
}
mcpServers={props.mcpServers}
sessionId={props.sessionId}
taskId={props.taskId}
taskTitle={props.taskTitle}
onAttachFiles={props.onAttachFiles}
contextPopoverOpen={props.contextPopoverOpen}
onContextPopoverOpenChange={props.onContextPopoverOpenChange ?? (() => {})}
contextCount={props.contextCount}
planContextEnabled={props.planContextEnabled}
contextFiles={props.contextFiles}
onToggleFile={props.onToggleFile ?? (() => {})}
isAgentBusy={props.isAgentBusy}
hasContent={props.hasContent ?? false}
onImplementPlan={props.onImplementPlan}
onEnhancePrompt={props.onEnhancePrompt}
isEnhancingPrompt={props.isEnhancingPrompt}
isUtilityConfigured={props.isUtilityConfigured}
isDisabled={props.isDisabled}
submitDisabledReason={props.submitDisabledReason}
isSending={props.isSending}
onCancel={props.onCancel}
onSubmit={props.onSubmit}
submitShortcut={submitShortcut}
onVoiceTranscript={props.onVoiceTranscript}
onVoiceAutoSend={props.onVoiceAutoSend}
/>
);
}
const { submitKey: _submitKey, ...desktopProps } = props;
return <DesktopChatInputToolbar {...desktopProps} submitShortcut={submitShortcut} />;
});