diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 8d95eb95..78433229 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -8,7 +8,7 @@ import z from 'zod'; import { Button } from './components/ui/button'; import { CheckIcon, LoaderIcon } from 'lucide-react'; import { MarkdownEditor } from './components/markdown-editor'; -import { ChatButton } from './components/chat-button'; +import { ChatInputBar } from './components/chat-button'; import { ChatSidebar } from './components/chat-sidebar'; import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view'; import { useDebounce } from './hooks/use-debounce'; @@ -592,6 +592,21 @@ function App() { } } + const handleNewChat = useCallback(() => { + setConversation([]) + setCurrentAssistantMessage('') + setCurrentReasoning('') + setRunId(null) + setMessage('') + setModelUsage(null) + }, []) + + const handleChatInputSubmit = (text: string) => { + setIsChatSidebarOpen(true) + // Submit immediately - the sidebar will open and show the message + handlePromptSubmit({ text }) + } + const toggleExpand = (path: string, kind: 'file' | 'dir') => { if (kind === 'file') { setSelectedPath(path) @@ -1126,6 +1141,7 @@ function App() { setIsChatSidebarOpen(false)} + onNewChat={handleNewChat} conversation={conversation} currentAssistantMessage={currentAssistantMessage} currentReasoning={currentReasoning} @@ -1140,9 +1156,12 @@ function App() { )} - {/* Floating chat button - shown when viewing files/graph and chat sidebar is closed */} + {/* Floating chat input - shown when viewing files/graph and chat sidebar is closed */} {(selectedPath || isGraphOpen) && !isChatSidebarOpen && ( - setIsChatSidebarOpen(true)} /> + setIsChatSidebarOpen(true)} + /> )} diff --git a/apps/x/apps/renderer/src/components/chat-button.tsx b/apps/x/apps/renderer/src/components/chat-button.tsx index b63b1bcb..a69cf3cc 100644 --- a/apps/x/apps/renderer/src/components/chat-button.tsx +++ b/apps/x/apps/renderer/src/components/chat-button.tsx @@ -1,18 +1,61 @@ -import { MessageSquare } from 'lucide-react' +import { useState } from 'react' +import { ArrowUp } from 'lucide-react' import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' -interface ChatButtonProps { - onClick: () => void +interface ChatInputBarProps { + onSubmit: (message: string) => void + onOpen: () => void } -export function ChatButton({ onClick }: ChatButtonProps) { +export function ChatInputBar({ onSubmit, onOpen }: ChatInputBarProps) { + const [message, setMessage] = useState('') + + const handleSubmit = () => { + const trimmed = message.trim() + if (trimmed) { + onSubmit(trimmed) + setMessage('') + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit() + } + } + + const handleFocus = () => { + onOpen() + } + return ( - +
+
+ setMessage(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={handleFocus} + placeholder="Ask anything..." + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> + +
+
) } diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 25061a7d..593adeb7 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -1,8 +1,13 @@ import { useCallback, useRef, useState } from 'react' -import { X } from 'lucide-react' -import type { ChatStatus, LanguageModelUsage, ToolUIPart } from 'ai' +import { ArrowUp, PanelRightClose, Plus } from 'lucide-react' +import type { LanguageModelUsage, ToolUIPart } from 'ai' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' import { Conversation, ConversationContent, @@ -14,29 +19,9 @@ import { MessageContent, MessageResponse, } from '@/components/ai-elements/message' -import { - PromptInput, - PromptInputBody, - PromptInputFooter, - type PromptInputMessage, - PromptInputSubmit, - PromptInputTextarea, - PromptInputTools, -} from '@/components/ai-elements/prompt-input' import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning' import { Shimmer } from '@/components/ai-elements/shimmer' import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool' -import { - Context, - ContextCacheUsage, - ContextContent, - ContextContentBody, - ContextContentHeader, - ContextInputUsage, - ContextOutputUsage, - ContextReasoningUsage, - ContextTrigger, -} from '@/components/ai-elements/context' interface ChatMessage { id: string @@ -114,13 +99,14 @@ const DEFAULT_WIDTH = 400 interface ChatSidebarProps { defaultWidth?: number onClose: () => void + onNewChat: () => void conversation: ConversationItem[] currentAssistantMessage: string currentReasoning: string isProcessing: boolean message: string onMessageChange: (message: string) => void - onSubmit: (message: PromptInputMessage) => void + onSubmit: (message: { text: string }) => void contextUsage: LanguageModelUsage maxTokens: number usedTokens: number @@ -129,6 +115,7 @@ interface ChatSidebarProps { export function ChatSidebar({ defaultWidth = DEFAULT_WIDTH, onClose, + onNewChat, conversation, currentAssistantMessage, currentReasoning, @@ -136,14 +123,12 @@ export function ChatSidebar({ message, onMessageChange, onSubmit, - contextUsage, - maxTokens, - usedTokens, }: ChatSidebarProps) { const [width, setWidth] = useState(defaultWidth) const [isResizing, setIsResizing] = useState(false) const startXRef = useRef(0) const startWidthRef = useRef(0) + const inputRef = useRef(null) const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault() @@ -152,7 +137,6 @@ export function ChatSidebar({ setIsResizing(true) const handleMouseMove = (e: MouseEvent) => { - // Since sidebar is on right, dragging left increases width const delta = startXRef.current - e.clientX const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + delta)) setWidth(newWidth) @@ -167,10 +151,24 @@ export function ChatSidebar({ document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [width]) + const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning - const submitStatus: ChatStatus = isProcessing ? 'streaming' : 'ready' const canSubmit = Boolean(message.trim()) && !isProcessing + const handleSubmit = () => { + const trimmed = message.trim() + if (trimmed && !isProcessing) { + onSubmit({ text: trimmed }) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit() + } + } + const renderConversationItem = (item: ConversationItem) => { if (isChatMessage(item)) { return ( @@ -234,22 +232,37 @@ export function ChatSidebar({ isResizing && "after:bg-primary" )} /> - {/* Header */} -
- Chat - + + {/* Header - minimal, no border */} +
+ + + + + Close + + + + + + New chat +
{/* Conversation area */} -
+
- + {!hasConversation ? ( -
- Ask anything... +
+
+ Ask anything... +
) : ( @@ -281,46 +294,36 @@ export function ChatSidebar({ )} - + - {/* Prompt input */} -
- - - onMessageChange(e.target.value)} - placeholder="Type your message..." - disabled={isProcessing} - className="min-h-[60px] max-h-[120px]" - /> - - - - - - - - - - - - - - - - - - - + {/* Input area - responsive to sidebar width, matches floating bar position exactly */} +
+
+ onMessageChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Ask anything..." + disabled={isProcessing} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:opacity-50" + /> + +