diff --git a/apps/x/apps/renderer/components.json b/apps/x/apps/renderer/components.json new file mode 100644 index 00000000..ffd5afa1 --- /dev/null +++ b/apps/x/apps/renderer/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/App.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/apps/x/apps/renderer/package.json b/apps/x/apps/renderer/package.json index 87069046..c37b6066 100644 --- a/apps/x/apps/renderer/package.json +++ b/apps/x/apps/renderer/package.json @@ -9,16 +9,36 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-use-controllable-state": "^1.2.2", "@tailwindcss/vite": "^4.1.18", "@x/preload": "workspace:*", "@x/shared": "workspace:*", + "ai": "^5.0.117", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^0.562.0", + "motion": "^12.23.26", + "nanoid": "^5.1.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "streamdown": "^1.6.10", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", + "tokenlens": "^1.3.1", + "use-stick-to-bottom": "^1.1.1", "zod": "^4.2.1" }, "devDependencies": { @@ -31,8 +51,9 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } -} \ No newline at end of file +} diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 0349bda7..05538f95 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -1,4 +1,7 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); #root { width: 100%; @@ -42,3 +45,122 @@ .read-the-docs { color: #888; } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 33153785..a8ce0c2c 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -1,11 +1,60 @@ -import { useCallback, useEffect, useState, useRef } from 'react' +import { useCallback, useEffect, useState } from 'react' import { workspace } from '@x/shared'; import { RunEvent } from '@x/shared/src/runs.js'; +import type { ChatStatus, LanguageModelUsage, ToolUIPart } from 'ai'; import './App.css' import z from 'zod'; import { Button } from './components/ui/button'; -import { Textarea } from './components/ui/textarea'; -import { Send, Loader2, MessageSquare } from 'lucide-react'; +import { MessageSquare } from 'lucide-react'; +import { AppSidebar } from '@/components/app-sidebar'; +import { + Conversation, + ConversationContent, + ConversationEmptyState, + ConversationScrollButton, +} from '@/components/ai-elements/conversation'; +import { + Message, + 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'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" type DirEntry = z.infer type RunEventType = z.infer @@ -25,8 +74,8 @@ interface ChatMessage { interface ToolCall { id: string; name: string; - input: unknown; - result?: unknown; + input: ToolUIPart['input']; + result?: ToolUIPart['output']; status: 'pending' | 'running' | 'completed' | 'error'; timestamp: number; } @@ -39,6 +88,73 @@ interface ReasoningBlock { type ConversationItem = ChatMessage | ToolCall | ReasoningBlock; +type ToolState = 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; + +const estimateTokens = (text: string) => { + if (!text) return 0 + return Math.ceil(text.trim().length / 4) +} + +const isChatMessage = (item: ConversationItem): item is ChatMessage => 'role' in item +const isToolCall = (item: ConversationItem): item is ToolCall => 'name' in item +const isReasoningBlock = (item: ConversationItem): item is ReasoningBlock => + 'content' in item && !('role' in item) && !('name' in item) + +const toToolState = (status: ToolCall['status']): ToolState => { + switch (status) { + case 'pending': + return 'input-streaming' + case 'running': + return 'input-available' + case 'completed': + return 'output-available' + case 'error': + return 'output-error' + default: + return 'input-available' + } +} + +const normalizeUsage = (usage?: Partial | null): LanguageModelUsage | null => { + if (!usage) return null + const hasNumbers = Object.values(usage).some((value) => typeof value === 'number') + if (!hasNumbers) return null + const inputTokens = usage.inputTokens ?? 0 + const outputTokens = usage.outputTokens ?? 0 + const reasoningTokens = usage.reasoningTokens ?? 0 + const totalTokens = usage.totalTokens ?? inputTokens + outputTokens + reasoningTokens + return { + inputTokens, + outputTokens, + totalTokens, + cachedInputTokens: usage.cachedInputTokens ?? 0, + reasoningTokens, + } +} + +const normalizeToolInput = (input: ToolCall['input'] | string | undefined): ToolCall['input'] => { + if (input === undefined || input === null) return {} + if (typeof input === 'string') { + const trimmed = input.trim() + if (!trimmed) return {} + try { + return JSON.parse(trimmed) + } catch { + return input + } + } + return input +} + +const normalizeToolOutput = (output: ToolCall['result'] | undefined, status: ToolCall['status']) => { + if (output === undefined || output === null) { + return status === 'completed' ? 'No output returned.' : null + } + if (output === '') return '(empty output)' + if (typeof output === 'boolean' || typeof output === 'number') return String(output) + return output +} + // Sort nodes (dirs first, then alphabetically) function sortNodes(nodes: TreeNode[]): TreeNode[] { return nodes.sort((a, b) => { @@ -90,28 +206,20 @@ function App() { const [expandedPaths, setExpandedPaths] = useState>(new Set()) const [selectedPath, setSelectedPath] = useState(null) const [fileContent, setFileContent] = useState('') - const [fileLoading, setFileLoading] = useState(true) - const [fileError, setFileError] = useState(null) // Chat state const [message, setMessage] = useState('') const [conversation, setConversation] = useState([]) const [currentAssistantMessage, setCurrentAssistantMessage] = useState('') const [currentReasoning, setCurrentReasoning] = useState('') + const [modelUsage, setModelUsage] = useState(null) const [runId, setRunId] = useState(null) const [isProcessing, setIsProcessing] = useState(false) const [agentId] = useState('copilot') - const messagesEndRef = useRef(null) - - // Scroll to bottom when conversation updates - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) - }, [conversation, currentAssistantMessage, currentReasoning]) // Load directory and merge into tree const loadDirectory = useCallback(async (path: string = '') => { try { - setFileError(null) const result = await window.ipc.invoke('workspace:readdir', { path, opts: { recursive: true, includeHidden: false } @@ -119,7 +227,7 @@ function App() { const tree = buildTree(result) return tree } catch (err) { - setFileError(String(err)) + console.error('Failed to load directory:', err) return [] } }, []) @@ -129,7 +237,6 @@ function App() { async function process() { const tree = await loadDirectory(); setTree(tree) - setFileLoading(false) } process(); }, [loadDirectory]) @@ -159,7 +266,7 @@ function App() { setFileContent('') } } catch (err) { - setFileError(String(err)) + console.error('Failed to load file:', err) } } process(); @@ -167,8 +274,6 @@ function App() { // Listen to run events useEffect(() => { - // Note: runs:events sends RunEvent data, but IPC contract types it as null - // We need to cast the handler to accept the actual event type const cleanup = window.ipc.on('runs:events', ((event: unknown) => { handleRunEvent(event as RunEventType) }) as (event: null) => void) @@ -176,7 +281,6 @@ function App() { }, [runId]) const handleRunEvent = (event: RunEventType) => { - // Only process events for the current run if (event.runId !== runId) return console.log('Run event:', event.type, event) @@ -184,6 +288,7 @@ function App() { switch (event.type) { case 'run-processing-start': setIsProcessing(true) + setModelUsage(null) break case 'run-processing-end': @@ -193,6 +298,7 @@ function App() { case 'start': setCurrentAssistantMessage('') setCurrentReasoning('') + setModelUsage(null) break case 'llm-stream-event': @@ -201,7 +307,6 @@ function App() { if (llmEvent.type === 'reasoning-delta' && llmEvent.delta) { setCurrentReasoning(prev => prev + llmEvent.delta) } else if (llmEvent.type === 'reasoning-end') { - // Commit reasoning block if we have content setCurrentReasoning(reasoning => { if (reasoning) { setConversation(prev => [...prev, { @@ -215,14 +320,18 @@ function App() { } else if (llmEvent.type === 'text-delta' && llmEvent.delta) { setCurrentAssistantMessage(prev => prev + llmEvent.delta) } else if (llmEvent.type === 'tool-call') { - // Add tool call to conversation setConversation(prev => [...prev, { id: llmEvent.toolCallId || `tool-${Date.now()}`, name: llmEvent.toolName || 'tool', - input: llmEvent.input, + input: normalizeToolInput(llmEvent.input as ToolUIPart['input']), status: 'running', timestamp: Date.now(), }]) + } else if (llmEvent.type === 'finish-step') { + const nextUsage = normalizeUsage(llmEvent.usage) + if (nextUsage) { + setModelUsage(nextUsage) + } } } break @@ -231,11 +340,9 @@ function App() { { const msg = event.message if (msg.role === 'assistant') { - // Commit current assistant message setCurrentAssistantMessage(currentMsg => { if (currentMsg) { setConversation(prev => { - // Avoid duplicates const exists = prev.some(m => m.id === event.messageId && 'role' in m && m.role === 'assistant' ) @@ -255,20 +362,66 @@ function App() { break case 'tool-invocation': - setConversation(prev => prev.map(item => - item.id === event.toolCallId || ('name' in item && item.name === event.toolName) - ? { ...item, status: 'running' as const } - : item - )) - break + { + const parsedInput = normalizeToolInput(event.input) + setConversation(prev => { + let matched = false + const next = prev.map(item => { + if ( + isToolCall(item) + && (event.toolCallId ? item.id === event.toolCallId : item.name === event.toolName) + ) { + matched = true + return { ...item, input: parsedInput, status: 'running' as const } + } + return item + }) + if (!matched) { + next.push({ + id: event.toolCallId ?? `tool-${Date.now()}`, + name: event.toolName, + input: parsedInput, + status: 'running', + timestamp: Date.now(), + }) + } + return next + }) + break + } case 'tool-result': - setConversation(prev => prev.map(item => - item.id === event.toolCallId || ('name' in item && item.name === event.toolName) - ? { ...item, result: event.result, status: 'completed' as const } - : item - )) - break + { + setConversation(prev => { + let matched = false + const next = prev.map(item => { + if ( + isToolCall(item) + && (event.toolCallId ? item.id === event.toolCallId : item.name === event.toolName) + ) { + matched = true + return { + ...item, + result: event.result as ToolUIPart['output'], + status: 'completed' as const, + } + } + return item + }) + if (!matched) { + next.push({ + id: event.toolCallId ?? `tool-${Date.now()}`, + name: event.toolName, + input: {}, + result: event.result as ToolUIPart['output'], + status: 'completed', + timestamp: Date.now(), + }) + } + return next + }) + break + } case 'error': setIsProcessing(false) @@ -277,14 +430,14 @@ function App() { } } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!message.trim() || isProcessing) return + const handlePromptSubmit = async ({ text }: PromptInputMessage) => { + if (isProcessing) return + + const userMessage = text.trim() + if (!userMessage) return - const userMessage = message.trim() setMessage('') - // Add user message immediately const userMessageId = `user-${Date.now()}` setConversation(prev => [...prev, { id: userMessageId, @@ -294,7 +447,6 @@ function App() { }]) try { - // Create run if needed let currentRunId = runId if (!currentRunId) { const run = await window.ipc.invoke('runs:create', { @@ -304,7 +456,6 @@ function App() { setRunId(currentRunId) } - // Send message await window.ipc.invoke('runs:createMessage', { runId: currentRunId, message: userMessage, @@ -329,292 +480,231 @@ function App() { setExpandedPaths(newExpanded) } - const handleCreateFile = async (parentPath: string = '') => { - const name = prompt('Enter file name:') - if (!name) return - - const filePath = parentPath ? `${parentPath}/${name}` : name - try { - await window.ipc.invoke('workspace:writeFile', { - path: filePath, - data: '', - opts: { - encoding: 'utf8' - }, - }) - } catch (err) { - setFileError(String(err)) - } - } - - const handleCreateDir = async (parentPath: string = '') => { - const name = prompt('Enter directory name:') - if (!name) return - - const dirPath = parentPath ? `${parentPath}/${name}` : name - try { - await window.ipc.invoke('workspace:mkdir', { - path: dirPath, - recursive: false - }) - if (parentPath) { - setExpandedPaths(prev => new Set(prev).add(parentPath)) - } - } catch (err) { - setFileError(String(err)) - } - } - - const handleDelete = async (path: string) => { - if (!confirm(`Delete ${path}?`)) return - - try { - await window.ipc.invoke('workspace:remove', { - path, - opts: { - recursive: true, - trash: true, - }, - }) - if (selectedPath === path) { - setSelectedPath(null) - } - } catch (err) { - setFileError(String(err)) - } - } - - const renderTreeNode = (node: TreeNode, depth: number = 0) => { - const isExpanded = expandedPaths.has(node.path) - const isSelected = selectedPath === node.path - const hasChildren = node.children && node.children.length > 0 - - return ( -
-
toggleExpand(node.path, node.kind)} - > - - {node.kind === 'dir' ? (isExpanded ? '📂' : '📁') : '📄'} - - {node.name} - -
- {node.kind === 'dir' && isExpanded && hasChildren && ( -
- {node.children!.map(child => renderTreeNode(child, depth + 1))} -
- )} -
- ) - } - const renderConversationItem = (item: ConversationItem) => { - if ('role' in item) { - // ChatMessage + if (isChatMessage(item)) { return ( -
-
-
{item.content}
-
-
- ) - } else if ('name' in item) { - // ToolCall - return ( -
-
- 🔧 {item.name} -
- {item.input !== undefined && item.input !== null && ( -
- Input: {JSON.stringify(item.input, null, 2)} -
- )} - {item.result !== undefined && ( -
- Result: {typeof item.result === 'string' ? item.result : String(JSON.stringify(item.result, null, 2)).substring(0, 1000)} -
- )} -
- Status: {item.status} -
-
- ) - } else { - // ReasoningBlock - return ( -
-
💭 Reasoning
-
{item.content}
-
+ + + {item.role === 'assistant' ? ( + {item.content} + ) : ( + item.content + )} + + ) } + + if (isToolCall(item)) { + const errorText = item.status === 'error' ? 'Tool error' : '' + const output = normalizeToolOutput(item.result, item.status) + const input = normalizeToolInput(item.input) + return ( + + + + + {output !== null ? ( + + ) : null} + + + ) + } + + if (isReasoningBlock(item)) { + return ( + + + {item.content} + + ) + } + + return null } + const chatMessages = conversation.filter(isChatMessage) + const reasoningBlocks = conversation.filter(isReasoningBlock) + const estimatedInputTokens = chatMessages + .filter((item) => item.role === 'user') + .reduce((total, item) => total + estimateTokens(item.content), 0) + const estimatedOutputTokens = chatMessages + .filter((item) => item.role === 'assistant') + .reduce((total, item) => total + estimateTokens(item.content), 0) + + estimateTokens(currentAssistantMessage) + const estimatedReasoningTokens = reasoningBlocks + .reduce((total, item) => total + estimateTokens(item.content), 0) + + estimateTokens(currentReasoning) + const estimatedTotalTokens = estimatedInputTokens + estimatedOutputTokens + estimatedReasoningTokens + const maxTokens = 128_000 + const estimatedUsage = { + inputTokens: estimatedInputTokens, + outputTokens: estimatedOutputTokens, + totalTokens: estimatedTotalTokens, + cachedInputTokens: 0, + reasoningTokens: estimatedReasoningTokens, + } as LanguageModelUsage + const effectiveUsage = modelUsage ?? estimatedUsage + const effectiveTotalTokens = effectiveUsage.totalTokens + ?? (effectiveUsage.inputTokens ?? 0) + + (effectiveUsage.outputTokens ?? 0) + + (effectiveUsage.reasoningTokens ?? 0) + const usedTokens = Math.min(effectiveTotalTokens, maxTokens) + const contextUsage = { + ...effectiveUsage, + totalTokens: effectiveTotalTokens, + } as LanguageModelUsage + + const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning + const submitStatus: ChatStatus = isProcessing ? 'streaming' : 'ready' + const canSubmit = Boolean(message.trim()) && !isProcessing + return ( -
- {/* Sidebar - File Browser */} -
-
- - -
-
- {fileLoading &&
Loading...
} - {fileError &&
{fileError}
} - {tree.map(node => renderTreeNode(node))} -
-
- - {/* Main content area */} -
- {/* File viewer or chat */} + + + +
+ + + + + + Workspace + + + + + {selectedPath ? selectedPath : 'Chat'} + + + + +
+ {selectedPath ? ( <> -
-
{selectedPath}
+
+
{selectedPath}
-
+              
                 {fileContent || 'Loading...'}
               
) : ( - <> - {/* Chat area */} -
-
- {conversation.length === 0 && !currentAssistantMessage && !currentReasoning ? ( -
-
-

- Start a conversation -

-

- Type a message below to begin chatting with the agent -

-
-
+
+ + + {!hasConversation ? ( + } + title="Start a conversation" + /> ) : ( <> {conversation.map(item => renderConversationItem(item))} - - {/* Current reasoning */} + {currentReasoning && ( -
-
💭 Reasoning
-
- {currentReasoning} - -
-
+ + + {currentReasoning} + )} - {/* Current streaming message */} {currentAssistantMessage && ( -
-
-
- {currentAssistantMessage} - -
-
-
+ + + {currentAssistantMessage} + + )} - {isProcessing && ( -
-
- - Processing... -
-
+ {isProcessing && !currentAssistantMessage && !currentReasoning && ( + + + Thinking... + + )} - -
)} + + + + +
+
+
+ + + setMessage(e.target.value)} + placeholder="Type your message..." + disabled={isProcessing} + /> + + + + + + + + + + + + + + + + + + +
- - {/* Input area */} -
-
-
-