From 1826d58402689d7ca20af7d0ffd0f7cd03bdd706 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:39:33 +0000 Subject: [PATCH] feat(cloud-agent): prompt autocomplete (ghost text) Implements ghost-text prompt autocomplete for the Cloud Agent chat input (based on original PR #5 by @markijbema, rebased on monorepo main). - Adds `cloudAgent.getFimAutocomplete` tRPC mutation using Mistral Codestral FIM - Adds `useChatGhostText` hook with debounced completion requests - Adds ghost text overlay rendering in `cloud-agent-next` ChatInput - Accept suggestion: Tab (all), ArrowRight (next word), Escape clears - Improves error handling in `sendProxiedChatCompletion` --- apps/web/src/app/globals.css | 8 + .../components/cloud-agent-next/ChatInput.tsx | 64 ++++- .../hooks/useChatGhostText.ts | 242 ++++++++++++++++++ .../src/lib/cloud-agent/chat-completion.ts | 131 ++++++++++ apps/web/src/lib/llm-proxy-helpers.ts | 35 ++- apps/web/src/routers/cloud-agent-router.ts | 28 ++ apps/web/src/routers/cloud-agent-schemas.ts | 7 + 7 files changed, 491 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/components/cloud-agent-next/hooks/useChatGhostText.ts create mode 100644 apps/web/src/lib/cloud-agent/chat-completion.ts diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 10210e8a03..9917d66dc0 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -233,3 +233,11 @@ } } } + +/* Ghost text for chat autocomplete */ +.chat-ghost-text { + color: var(--muted-foreground); + opacity: 0.6; + pointer-events: none; + user-select: none; +} diff --git a/apps/web/src/components/cloud-agent-next/ChatInput.tsx b/apps/web/src/components/cloud-agent-next/ChatInput.tsx index 0b61ad4041..d4f167bc46 100644 --- a/apps/web/src/components/cloud-agent-next/ChatInput.tsx +++ b/apps/web/src/components/cloud-agent-next/ChatInput.tsx @@ -14,6 +14,7 @@ import { ModelCombobox, type ModelOption } from '@/components/shared/ModelCombob import { VariantCombobox } from '@/components/shared/VariantCombobox'; import { MobileToolbarPopover } from './MobileToolbarPopover'; import type { AgentMode } from './types'; +import { useChatGhostText } from './hooks/useChatGhostText'; type ChatInputProps = { onSend: (message: string) => void; @@ -44,6 +45,7 @@ type ChatInputProps = { showToolbar?: boolean; /** Pre-populate the textarea (e.g. to restore text after a failed send) */ initialValue?: string; + enableChatAutocomplete?: boolean; }; export function ChatInput({ @@ -64,12 +66,22 @@ export function ChatInput({ availableVariants = [], showToolbar = false, initialValue, + 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, + }); + // Restore text into the textarea when initialValue changes (e.g. after a failed send). // Treats undefined as "no opinion" (skip), but empty string actively clears the field. useEffect(() => { @@ -121,6 +133,7 @@ export function ChatInput({ onSend(trimmed); setValue(''); + handleGhostTextInputChange(''); setShowAutocomplete(false); if (textareaRef.current) { @@ -143,12 +156,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'; @@ -164,6 +179,10 @@ export function ChatInput({ // Ignore keyboard events during IME composition (Chinese, Japanese, Korean input) if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return; + if (handleGhostTextKeyDown(e)) { + return; + } + if (showAutocomplete && filteredCommands.length > 0) { switch (e.key) { case 'ArrowDown': @@ -224,20 +243,37 @@ export function ChatInput({ {/* Textarea with slash command autocomplete */} -