diff --git a/src/app/globals.css b/src/app/globals.css index c4fb61128..4b3864604 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -182,3 +182,11 @@ animation: pulse-glow 0.6s ease-out; } } + +/* Ghost text for chat autocomplete */ +.chat-ghost-text { + color: var(--muted-foreground); + opacity: 0.6; + pointer-events: none; + user-select: none; +} diff --git a/src/components/cloud-agent/ChatInput.tsx b/src/components/cloud-agent/ChatInput.tsx index 1d3ebdb30..7d5a452b8 100644 --- a/src/components/cloud-agent/ChatInput.tsx +++ b/src/components/cloud-agent/ChatInput.tsx @@ -13,6 +13,7 @@ import { BrowseCommandsDialog } from './BrowseCommandsDialog'; import { ModeCombobox } from '@/components/shared/ModeCombobox'; import { ModelCombobox, type ModelOption } from '@/components/shared/ModelCombobox'; import type { AgentMode } from './types'; +import { useChatGhostText } from './hooks/useChatGhostText'; type ChatInputProps = { onSend: (message: string) => void; @@ -35,6 +36,7 @@ type ChatInputProps = { onModelChange?: (model: string) => void; /** Whether to show the toolbar (hide when no active session) */ showToolbar?: boolean; + enableChatAutocomplete?: boolean; }; export function ChatInput({ @@ -51,12 +53,22 @@ export function ChatInput({ onModeChange, onModelChange, showToolbar = false, + enableChatAutocomplete = true, }: ChatInputProps) { const [value, setValue] = useState(''); const [showAutocomplete, setShowAutocomplete] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const textareaRef = useRef(null); + const { + ghostText, + handleKeyDown: handleGhostTextKeyDown, + handleInputChange: handleGhostTextInputChange, + } = useChatGhostText({ + textAreaRef: textareaRef, + enableChatAutocomplete: enableChatAutocomplete && !disabled && !isStreaming, + }); + // Filter commands based on current input const filteredCommands = useMemo(() => { if (!slashCommands || slashCommands.length === 0) return []; @@ -99,6 +111,7 @@ export function ChatInput({ onSend(trimmed); setValue(''); + handleGhostTextInputChange(''); setShowAutocomplete(false); if (textareaRef.current) { @@ -121,12 +134,14 @@ export function ChatInput({ // Send immediately onSend(expansion); setValue(''); + handleGhostTextInputChange(''); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; } } else { // Just fill the input for editing setValue(expansion); + handleGhostTextInputChange(expansion); // Force height recalculation for expanded text if (textareaRef.current) { textareaRef.current.style.height = 'auto'; @@ -139,6 +154,10 @@ export function ChatInput({ }; const handleKeyDown = (e: KeyboardEvent) => { + if (handleGhostTextKeyDown(e)) { + return; + } + if (showAutocomplete && filteredCommands.length > 0) { switch (e.key) { case 'ArrowDown': @@ -213,20 +232,39 @@ export function ChatInput({
-