Files
kandev/apps/web/components/task/chat/chat-input-toolbar.tsx
T
Carlos Florêncio f4d1dfd3a7 feat: better context files (#222)
feat: context files
2026-02-14 20:13:11 +00:00

167 lines
5.3 KiB
TypeScript

'use client';
import { memo } from 'react';
import {
IconArrowUp,
IconFileTextSpark,
IconPlayerPauseFilled,
IconAt,
} from '@tabler/icons-react';
import { GridSpinner } from '@/components/grid-spinner';
import { Button } from '@kandev/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { cn } from '@/lib/utils';
import { SHORTCUTS } from '@/lib/keyboard/constants';
import { KeyboardShortcutTooltip } from '@/components/keyboard-shortcut-tooltip';
import { TokenUsageDisplay } from '@/components/task/chat/token-usage-display';
import { SessionsDropdown } from '@/components/task/sessions-dropdown';
import { ModelSelector } from '@/components/task/model-selector';
import { ContextPopover } from './context-popover';
import type { ContextFile } from '@/lib/state/context-files-store';
export type ChatInputToolbarProps = {
planModeEnabled: boolean;
onPlanModeChange: (enabled: boolean) => void;
sessionId: string | null;
taskId: string | null;
taskTitle?: string;
taskDescription: string;
isAgentBusy: boolean;
isDisabled: boolean;
isSending: boolean;
onCancel: () => 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;
};
export const ChatInputToolbar = memo(function ChatInputToolbar({
planModeEnabled,
onPlanModeChange,
sessionId,
taskId,
taskTitle,
taskDescription,
isAgentBusy,
isDisabled,
isSending,
onCancel,
onSubmit,
submitKey = 'cmd_enter',
contextCount = 0,
contextPopoverOpen = false,
onContextPopoverOpenChange,
planContextEnabled = false,
contextFiles = [],
onToggleFile,
}: ChatInputToolbarProps) {
const submitShortcut = submitKey === 'enter' ? SHORTCUTS.SUBMIT_ENTER : SHORTCUTS.SUBMIT;
return (
<div className="flex items-center gap-1 px-1 pt-0 pb-0.5 border-t border-border">
{/* Left: Plan, Thinking, Context */}
<div className="flex items-center gap-0.5">
{/* Plan mode toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
'h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40',
planModeEnabled && 'bg-primary/15 text-primary'
)}
onClick={() => onPlanModeChange(!planModeEnabled)}
>
<IconFileTextSpark className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Toggle plan mode</TooltipContent>
</Tooltip>
{/* Context @ button */}
<ContextPopover
open={contextPopoverOpen}
onOpenChange={onContextPopoverOpenChange ?? (() => {})}
trigger={
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40 relative"
>
<IconAt className="h-4 w-4" />
{contextCount > 0 && (
<span className="absolute -top-1 -right-1 h-4 min-w-4 rounded-full bg-muted-foreground/80 text-[10px] text-background flex items-center justify-center px-0.5">
{contextCount}
</span>
)}
</Button>
}
sessionId={sessionId}
planContextEnabled={planContextEnabled}
contextFiles={contextFiles}
onToggleFile={onToggleFile ?? (() => {})}
/>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Right: Sessions, Model, Submit */}
<div className="flex items-center gap-0.5 shrink-0">
<SessionsDropdown
taskId={taskId}
activeSessionId={sessionId}
taskTitle={taskTitle}
taskDescription={taskDescription}
/>
{/* Token usage display */}
<TokenUsageDisplay sessionId={sessionId} />
<ModelSelector sessionId={sessionId} />
{/* Submit/Stop button */}
<div className="ml-1">
<KeyboardShortcutTooltip shortcut={submitShortcut} enabled={!isAgentBusy && !isDisabled}>
{isAgentBusy ? (
<Button
type="button"
variant="secondary"
size="icon"
className="h-7 w-7 rounded-full cursor-pointer bg-destructive/10 text-destructive hover:bg-destructive/20"
onClick={onCancel}
>
<IconPlayerPauseFilled className="h-3.5 w-3.5" />
</Button>
) : (
<Button
type="button"
variant="default"
size="icon"
className="h-7 w-7 rounded-full cursor-pointer"
disabled={isDisabled}
onClick={onSubmit}
>
{isSending ? (
<GridSpinner className="text-primary-foreground" />
) : (
<IconArrowUp className="h-4 w-4" />
)}
</Button>
)}
</KeyboardShortcutTooltip>
</div>
</div>
</div>
);
});