Add top_p/top_k/min_p/presence_penalty to AGENTS.md frontmatter and thread through inference (agents.ts parser → Agent type → stream-phase → sentinel summaries). Null means omit from request body, preserving provider defaults. Wire ask_user_input interactive card into both BooCoder frontends: the CoderPane in BooChat's SPA (CoderMessageList now renders AskUserInputCard instead of ToolCallLine for ask_user_input tool calls) and the standalone coder SPA (MessageBubble + new AskUserInputCard + shadcn ui primitives). Additional fixes: SessionLandingPage uses ChatInput with slash-command support and lazy chat creation; Session.tsx hydrate-race fix for empty pane promotion; AgentPicker wider dropdown with line-clamp; ModelPicker min-width; Textarea converted to forwardRef; Recon agent added to AGENTS.md; codecontext host port exposed in docker-compose. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { Send, Square } from 'lucide-react';
|
|
import type { Message, ToolResult } from '@/api/types';
|
|
import { api } from '@/api/client';
|
|
import { MessageBubble } from './MessageBubble';
|
|
|
|
interface Props {
|
|
sessionId: string;
|
|
chatId: string;
|
|
messages: Message[];
|
|
isStreaming: boolean;
|
|
connected: boolean;
|
|
}
|
|
|
|
export function ChatPane({ sessionId, chatId, messages, isStreaming, connected }: Props) {
|
|
const [input, setInput] = useState('');
|
|
const [sending, setSending] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
// Auto-scroll to bottom when messages change
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages]);
|
|
|
|
// Auto-resize textarea
|
|
useEffect(() => {
|
|
const el = textareaRef.current;
|
|
if (!el) return;
|
|
el.style.height = 'auto';
|
|
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
|
|
}, [input]);
|
|
|
|
const handleSend = async () => {
|
|
const content = input.trim();
|
|
if (!content || sending || isStreaming) return;
|
|
|
|
setInput('');
|
|
setSending(true);
|
|
try {
|
|
await api.messages.send(sessionId, chatId, content);
|
|
} catch (err) {
|
|
console.error('send failed:', err);
|
|
// Restore input on failure
|
|
setInput(content);
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
const handleStop = async () => {
|
|
try {
|
|
await api.messages.stop(sessionId);
|
|
} catch (err) {
|
|
console.error('stop failed:', err);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
};
|
|
|
|
// Filter out system messages for display (sentinels)
|
|
const visibleMessages = messages.filter((m) => m.role !== 'system');
|
|
|
|
// Build a lookup map from tool_call_id -> ToolResult for all messages
|
|
const toolResultsMap: Record<string, ToolResult> = {};
|
|
for (const msg of messages) {
|
|
if (msg.tool_results) {
|
|
toolResultsMap[msg.tool_results.tool_call_id] = msg.tool_results;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Connection indicator */}
|
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-zinc-800 text-xs text-zinc-500">
|
|
<div
|
|
className={`w-1.5 h-1.5 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
|
|
/>
|
|
<span>{connected ? 'Connected' : 'Disconnected'}</span>
|
|
{isStreaming && (
|
|
<span className="text-blue-400 ml-auto">Generating...</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Messages list */}
|
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
|
{visibleMessages.length === 0 && (
|
|
<div className="text-center text-zinc-500 mt-8">
|
|
<p className="text-lg font-medium">BooCoder</p>
|
|
<p className="text-sm mt-1">Send a message to start coding.</p>
|
|
</div>
|
|
)}
|
|
{visibleMessages.map((msg) => (
|
|
<MessageBubble key={msg.id} message={msg} chatId={msg.chat_id} toolResultsMap={toolResultsMap} />
|
|
))}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input area */}
|
|
<div className="border-t border-zinc-800 px-4 py-3">
|
|
<div className="flex items-end gap-2">
|
|
<textarea
|
|
ref={textareaRef}
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Message BooCoder..."
|
|
rows={1}
|
|
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 resize-none focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
|
disabled={sending}
|
|
/>
|
|
{isStreaming ? (
|
|
<button
|
|
onClick={handleStop}
|
|
className="p-2 rounded-lg bg-red-600 hover:bg-red-500 text-white transition-colors"
|
|
title="Stop generation"
|
|
>
|
|
<Square size={18} />
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={!input.trim() || sending}
|
|
className="p-2 rounded-lg bg-blue-600 hover:bg-blue-500 disabled:opacity-40 disabled:cursor-not-allowed text-white transition-colors"
|
|
title="Send message"
|
|
>
|
|
<Send size={18} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|