From 1950522a188d6c150b1bd019fabc97be3041f19e Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 4 Feb 2026 10:23:33 +0100 Subject: [PATCH] Cloud Agent: prompt autocomplete (ghost text) Implements ghost-text prompt autocomplete for the Cloud Agent chat input. - Adds cloudAgent.getChatCompletion tRPC mutation - Adds client-side ghost text hook and overlay rendering in ChatInput - Adds minimal CSS for ghost text styling - Improves error handling in llm-proxy-helpers Accept suggestion: Tab (all), ArrowRight (next word), Escape clears. --- src/app/globals.css | 8 + src/components/cloud-agent/ChatInput.tsx | 66 ++++- .../cloud-agent/hooks/useChatGhostText.ts | 259 ++++++++++++++++++ src/lib/cloud-agent/chat-completion.ts | 131 +++++++++ src/lib/llm-proxy-helpers.ts | 35 ++- src/routers/cloud-agent-router.ts | 27 ++ src/routers/cloud-agent-schemas.ts | 7 + 7 files changed, 509 insertions(+), 24 deletions(-) create mode 100644 src/components/cloud-agent/hooks/useChatGhostText.ts create mode 100644 src/lib/cloud-agent/chat-completion.ts 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({
-