From 0f450a4c172ae9842d9f2ee98d6f6121bd98eae4 Mon Sep 17 00:00:00 2001 From: Andrew Sliva Date: Thu, 22 Jan 2026 12:18:50 -0700 Subject: [PATCH 1/4] fix context search functionality --- ui/src/ChatApp.tsx | 21 +++- ui/src/components/MessageInput.tsx | 171 ++++++++++++++++++++--------- ui/src/components/MessageList.tsx | 13 +++ ui/src/hooks/useChat.ts | 67 ++++++++++- 4 files changed, 212 insertions(+), 60 deletions(-) diff --git a/ui/src/ChatApp.tsx b/ui/src/ChatApp.tsx index 153486e..8f1755f 100644 --- a/ui/src/ChatApp.tsx +++ b/ui/src/ChatApp.tsx @@ -14,7 +14,10 @@ export function ChatApp() { submitFeedback, requestAuth, getFeedbackStatus, - getPendingFeedback + getPendingFeedback, + attachments, + pickFiles, + removeAttachment } = useChat(); const messagesEndRef = useRef(null); @@ -60,8 +63,17 @@ export function ChatApp() { {/* Error Display */} {error && ( -
-

{error}

+
+
+ +

{error}

+
)} @@ -89,6 +101,9 @@ export function ChatApp() { onSend={(msg, attachments) => sendMessage(msg, true, attachments)} disabled={loading} fileContext={fileContext} + attachments={attachments} + pickFiles={pickFiles} + removeAttachment={removeAttachment} />
); diff --git a/ui/src/components/MessageInput.tsx b/ui/src/components/MessageInput.tsx index 7fecbcc..9dd6b3e 100644 --- a/ui/src/components/MessageInput.tsx +++ b/ui/src/components/MessageInput.tsx @@ -1,5 +1,6 @@ -import { useState, KeyboardEvent, useRef } from 'react'; -import { FileContext } from '../hooks/useChat'; +import { useState, KeyboardEvent, useEffect, useRef } from 'react'; +import { FileContext, Attachment } from '../hooks/useChat'; +import { vscodeApi, setupMessageListener, VSCodeMessage } from '../utils/vscodeApi'; function PaperclipIcon() { return ( @@ -9,53 +10,111 @@ function PaperclipIcon() { ); } -interface Attachment { - fileName: string; - content: string; -} - interface MessageInputProps { onSend: (message: string, attachments: Attachment[]) => void; disabled?: boolean; placeholder?: string; fileContext?: FileContext | null; + attachments: Attachment[]; + pickFiles: () => void; + removeAttachment: (fileName: string) => void; } -export function MessageInput({ onSend, disabled = false, placeholder = "Type your message...", fileContext }: MessageInputProps) { +export function MessageInput({ onSend, disabled = false, placeholder = "Type your message...", fileContext, attachments, pickFiles, removeAttachment }: MessageInputProps) { const [message, setMessage] = useState(''); - const [attachments, setAttachments] = useState([]); - const fileInputRef = useRef(null); + const [contextPickerActive, setContextPickerActive] = useState(false); + const textareaRef = useRef(null); + const hashPositionRef = useRef(-1); + + useEffect(() => { + const handleMessage = (msg: VSCodeMessage) => { + if (msg.command === 'contextPickerResult') { + setContextPickerActive(false); + + if (msg.result && textareaRef.current) { + // Insert the reference into the input at the hash position + const textarea = textareaRef.current; + const currentValue = textarea.value; + const cursorPos = textarea.selectionStart; + + // Find the hash position + if (hashPositionRef.current >= 0) { + const beforeHash = currentValue.substring(0, hashPositionRef.current); + const afterCursor = currentValue.substring(cursorPos); + const reference = msg.result.reference || `#${msg.result.file || msg.result.name}`; + + const newValue = beforeHash + reference + ' ' + afterCursor; + setMessage(newValue); + + // Set cursor position after the inserted reference + setTimeout(() => { + const newCursorPos = hashPositionRef.current + reference.length + 1; + textarea.setSelectionRange(newCursorPos, newCursorPos); + }, 0); + } + + hashPositionRef.current = -1; + } + } + }; + setupMessageListener(handleMessage); + }, []); const handleSend = () => { if (message.trim() && !disabled) { onSend(message, attachments); setMessage(''); - setAttachments([]); + // Don't clear attachments - they should persist for visibility } }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter' && !e.shiftKey && !contextPickerActive) { e.preventDefault(); handleSend(); } }; - const handleFileSelect = async (files: FileList | null) => { - if (!files) return; - const newAttachments: Attachment[] = []; - for (const file of Array.from(files)) { - if (file.size > 1024 * 1024) continue; - const text = await file.text(); - newAttachments.push({ fileName: file.name, content: text }); - } - if (newAttachments.length) { - setAttachments(prev => [...prev, ...newAttachments]); + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const cursorPos = e.target.selectionStart; + + // Check if "#" was just typed + if (value.length > 0 && cursorPos > 0 && value[cursorPos - 1] === '#') { + // Store the hash position + hashPositionRef.current = cursorPos - 1; + + // Extract query after "#" (if any) + const textAfterHash = value.substring(cursorPos); + const spaceIndex = textAfterHash.indexOf(' '); + const query = spaceIndex > 0 ? textAfterHash.substring(0, spaceIndex) : ''; + + // Trigger context picker + setContextPickerActive(true); + vscodeApi.postMessage({ + command: 'openContextPicker', + query: query + }); + } else if (contextPickerActive && value.length > 0) { + // Update query as user types after "#" + const hashIndex = value.lastIndexOf('#', cursorPos - 1); + if (hashIndex >= 0) { + const textAfterHash = value.substring(hashIndex + 1, cursorPos); + const spaceIndex = textAfterHash.indexOf(' '); + const query = spaceIndex > 0 ? textAfterHash.substring(0, spaceIndex) : textAfterHash; + + // Update context picker query + vscodeApi.postMessage({ + command: 'openContextPicker', + query: query + }); + } + } else { + setContextPickerActive(false); + hashPositionRef.current = -1; } - }; - - const removeAttachment = (name: string) => { - setAttachments(prev => prev.filter(a => a.fileName !== name)); + + setMessage(value); }; return ( @@ -73,10 +132,35 @@ export function MessageInput({ onSend, disabled = false, placeholder = "Type you )} + {/* Attachments display above input area */} + {attachments.length > 0 && ( +
+ {attachments.map(att => ( + + {att.fileName} + + + ))} +
+ )}
- handleFileSelect(e.target.files)} - /> - {attachments.length > 0 && ( -
- {attachments.map(att => ( - - {att.fileName} - - - ))} -
- )}