diff --git a/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx b/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx
index 99237e6..61b496e 100644
--- a/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx
+++ b/apps/web/src/app/dashboard/agents/[id]/chat/[chatId]/page.tsx
@@ -1,6 +1,7 @@
'use client';
import type { AIProvider } from '@tpmjs/types/agent';
+import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
@@ -8,6 +9,11 @@ import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Streamdown } from 'streamdown';
+import type { AgentSettings } from '~/components/agents/ChatSettingsDrawer';
+import { ChatSettingsDrawer } from '~/components/agents/ChatSettingsDrawer';
+import type { CollectionInfo, ToolInfo } from '~/components/agents/ChatToolsPanel';
+import { ChatToolsPanel } from '~/components/agents/ChatToolsPanel';
+import { ToolDetailsModal } from '~/components/agents/ToolDetailsModal';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
interface Agent {
@@ -17,6 +23,12 @@ interface Agent {
description: string | null;
provider: AIProvider;
modelId: string;
+ systemPrompt: string | null;
+ temperature: number;
+ maxToolCallsPerTurn: number;
+ maxMessagesInContext: number;
+ tools: ToolInfo[];
+ collections: CollectionInfo[];
}
interface MessageToolCall {
@@ -71,7 +83,7 @@ function getErrorMessage(output: unknown): string | null {
}
/**
- * Sexy tool call debug card component
+ * Tool call card with style guide aesthetics
*/
function ToolCallCard({
toolCall,
@@ -82,7 +94,6 @@ function ToolCallCard({
isExpanded: boolean;
onToggle: () => void;
}) {
- // Detect if the output contains an error
const hasError = Boolean(toolCall.output && isToolError(toolCall.output));
const errorMessage = hasError ? getErrorMessage(toolCall.output) : null;
const effectiveStatus = hasError ? 'error' : toolCall.status;
@@ -110,10 +121,12 @@ function ToolCallCard({
};
return (
-
- {/* Header */}
+ {/* header */}
-
+
- {toolCall.toolName}
+ {toolCall.toolName}
{toolCall.toolCallId.slice(0, 8)}...
{hasError && (
-
- ERROR
+
+ error
)}
- {/* Show error message preview in header */}
{hasError && errorMessage && !isExpanded && (
-
- {errorMessage}
-
+
{errorMessage}
)}
- {/* Expanded Content */}
+ {/* expanded content */}
{isExpanded && (
-
- {/* Input Section */}
+
+ {/* input */}
{toolCall.input !== undefined && toolCall.input !== null ? (
-
+
@@ -170,25 +180,25 @@ function ToolCallCard({
) : null}
- {/* Error Message Section */}
+ {/* error message */}
{hasError && errorMessage && (
-
+
- Error
+ error
{errorMessage}
)}
- {/* Output Section */}
+ {/* output */}
{toolCall.output !== undefined && toolCall.output !== null ? (
- {hasError ? 'Full Response' : 'Output'}
+ {hasError ? 'full response' : 'output'}
@@ -200,16 +210,16 @@ function ToolCallCard({
) : null}
- {/* Status indicator for running */}
+ {/* running indicator */}
{toolCall.status === 'running' && !toolCall.output && (
- Executing...
+ executing...
)}
)}
-
+
);
}
@@ -241,7 +251,6 @@ export default function AgentChatPage(): React.ReactElement {
const [agent, setAgent] = useState
(null);
const [conversations, setConversations] = useState([]);
- // Use chatId from URL as the active conversation
const activeConversationId = chatId;
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
@@ -252,6 +261,11 @@ export default function AgentChatPage(): React.ReactElement {
const [toolCalls, setToolCalls] = useState([]);
const [expandedToolCalls, setExpandedToolCalls] = useState>(new Set());
+ // UI state
+ const [showToolsPanel, setShowToolsPanel] = useState(true);
+ const [showSettings, setShowSettings] = useState(false);
+ const [selectedTool, setSelectedTool] = useState(null);
+
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
@@ -314,11 +328,6 @@ export default function AgentChatPage(): React.ReactElement {
if (data.success) {
const msgs = data.data.messages || [];
- console.log(
- '[fetchMessages] Received messages:',
- msgs.length,
- msgs.map((m: Message) => ({ role: m.role, toolName: m.toolName }))
- );
setMessages(msgs);
}
} catch (err) {
@@ -352,7 +361,6 @@ export default function AgentChatPage(): React.ReactElement {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
});
- // Navigate to conversation when selected from sidebar
const handleSelectConversation = (convSlug: string) => {
router.push(`/dashboard/agents/${agentId}/chat/${convSlug}`);
};
@@ -367,7 +375,6 @@ export default function AgentChatPage(): React.ReactElement {
setError(null);
setToolCalls([]);
- // Use the chatId from URL
const conversationId = activeConversationId || chatId;
// Optimistically add user message
@@ -406,7 +413,7 @@ export default function AgentChatPage(): React.ReactElement {
// Parse SSE events
const lines = buffer.split('\n');
- buffer = lines.pop() || ''; // Keep incomplete line in buffer
+ buffer = lines.pop() || '';
let eventType = '';
for (const line of lines) {
@@ -420,7 +427,6 @@ export default function AgentChatPage(): React.ReactElement {
setStreamingContent((prev) => prev + data.text);
break;
case 'tool_call':
- // Add tool call to tracking
setToolCalls((prev) => [
...prev,
{
@@ -430,11 +436,9 @@ export default function AgentChatPage(): React.ReactElement {
status: 'running',
},
]);
- // Auto-expand new tool calls
setExpandedToolCalls((prev) => new Set([...prev, data.toolCallId]));
break;
case 'tool_result':
- // Update tool call with result
setToolCalls((prev) =>
prev.map((tc) =>
tc.toolCallId === data.toolCallId
@@ -444,13 +448,10 @@ export default function AgentChatPage(): React.ReactElement {
);
break;
case 'complete':
- // Refresh messages
- console.log('[SSE] Complete event received, fetching messages...');
await fetchMessages();
await fetchConversations();
setStreamingContent('');
setToolCalls([]);
- console.log('[SSE] Messages and conversations refreshed');
break;
case 'error':
throw new Error(data.message);
@@ -461,7 +462,6 @@ export default function AgentChatPage(): React.ReactElement {
} catch (err) {
console.error('Failed to send message:', err);
setError(err instanceof Error ? err.message : 'Failed to send message');
- // Remove optimistic message on error
setMessages((prev) => prev.filter((m) => m.id !== userMessage.id));
} finally {
setIsSending(false);
@@ -482,6 +482,15 @@ export default function AgentChatPage(): React.ReactElement {
router.push(`/dashboard/agents/${agentId}/chat/${newId}`);
};
+ const handleSettingsChange = (newSettings: AgentSettings) => {
+ if (agent) {
+ setAgent({
+ ...agent,
+ ...newSettings,
+ });
+ }
+ };
+
if (isLoading) {
return (
+
+
@@ -561,50 +593,57 @@ export default function AgentChatPage(): React.ReactElement {
>
{/* Conversations Sidebar */}
-
- {/* Conversations List */}
+
+
+
+ conversations
+
+
{conversations.length === 0 ? (
-
- No conversations yet
+
+ no conversations yet
) : (
conversations.map((conv) => (
-
+
{conv.title || 'Untitled Chat'}
+
+ {conv.messageCount} messages
+
+
))
)}
{/* Chat Area */}
-
+
{/* Messages */}
{messages.length === 0 && !streamingContent && (
-
-
+
+
)}
@@ -628,7 +667,6 @@ export default function AgentChatPage(): React.ReactElement {
}
return messages.map((message) => {
- // Check if this ASSISTANT message has embedded tool calls
const hasEmbeddedToolCalls =
message.role === 'ASSISTANT' &&
message.toolCalls &&
@@ -641,13 +679,12 @@ export default function AgentChatPage(): React.ReactElement {
{hasEmbeddedToolCalls && (
{message.toolCalls?.map((tc) => {
- // Look up the output for this tool call
const toolOutput = toolResultsMap.get(tc.toolCallId);
const hasOutput = toolOutput !== undefined;
return (
-
+
)}
- {/* Render the message content - skip TOOL messages as they're shown with their calls */}
+ {/* Render the message content - skip TOOL messages */}
{message.role !== 'TOOL' && (
{message.role === 'USER' ? (
@@ -698,7 +735,7 @@ export default function AgentChatPage(): React.ReactElement {
{toolCalls.map((tc) => (
-
+
-
+
{streamingContent}
@@ -723,10 +760,10 @@ export default function AgentChatPage(): React.ReactElement {
{isSending && !streamingContent && toolCalls.length === 0 && (
-
+
- Thinking...
+ thinking...
@@ -737,13 +774,13 @@ export default function AgentChatPage(): React.ReactElement {
{/* Error Message */}
{error && (
-
-
{error}
+
)}
{/* Input Area */}
-
+
-
- Press Enter to send, Shift+Enter for new line
+
+ enter to send, shift+enter for new line
+
+ {/* Tools Panel */}
+
setShowToolsPanel(false)}
+ onToolClick={setSelectedTool}
+ />
+
+ {/* Tool Details Modal */}
+
setSelectedTool(null)}
+ />
+
+ {/* Settings Drawer */}
+ setShowSettings(false)}
+ agentId={agent.id}
+ settings={{
+ name: agent.name,
+ provider: agent.provider,
+ modelId: agent.modelId,
+ systemPrompt: agent.systemPrompt,
+ temperature: agent.temperature,
+ maxToolCallsPerTurn: agent.maxToolCallsPerTurn,
+ maxMessagesInContext: agent.maxMessagesInContext,
+ }}
+ onSettingsChange={handleSettingsChange}
+ />
);
}
diff --git a/apps/web/src/components/agents/ChatSettingsDrawer.tsx b/apps/web/src/components/agents/ChatSettingsDrawer.tsx
new file mode 100644
index 0000000..add56aa
--- /dev/null
+++ b/apps/web/src/components/agents/ChatSettingsDrawer.tsx
@@ -0,0 +1,274 @@
+'use client';
+
+import type { AIProvider } from '@tpmjs/types/agent';
+import { PROVIDER_MODELS, SUPPORTED_PROVIDERS } from '@tpmjs/types/agent';
+import { Button } from '@tpmjs/ui/Button/Button';
+import { Icon } from '@tpmjs/ui/Icon/Icon';
+import { Input } from '@tpmjs/ui/Input/Input';
+import { Label } from '@tpmjs/ui/Label/Label';
+import { Modal } from '@tpmjs/ui/Modal/Modal';
+import { Select } from '@tpmjs/ui/Select/Select';
+import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
+import { useCallback, useState } from 'react';
+
+const PROVIDER_DISPLAY_NAMES: Record = {
+ OPENAI: 'OpenAI',
+ ANTHROPIC: 'Anthropic',
+ GOOGLE: 'Google',
+ GROQ: 'Groq',
+ MISTRAL: 'Mistral',
+};
+
+export interface AgentSettings {
+ name: string;
+ provider: AIProvider;
+ modelId: string;
+ systemPrompt: string | null;
+ temperature: number;
+ maxToolCallsPerTurn: number;
+ maxMessagesInContext: number;
+}
+
+interface ChatSettingsDrawerProps {
+ open: boolean;
+ onClose: () => void;
+ agentId: string;
+ settings: AgentSettings;
+ onSettingsChange: (settings: AgentSettings) => void;
+}
+
+export function ChatSettingsDrawer({
+ open,
+ onClose,
+ agentId,
+ settings,
+ onSettingsChange,
+}: ChatSettingsDrawerProps) {
+ const [formData, setFormData] = useState(settings);
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Sync formData when settings prop changes
+ const syncFormData = useCallback(() => {
+ setFormData(settings);
+ }, [settings]);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sync form when modal opens
+ useState(() => {
+ if (open) syncFormData();
+ });
+
+ const handleChange = (
+ e: React.ChangeEvent
+ ) => {
+ const { name, value } = e.target;
+ setFormData((prev) => {
+ const newData = { ...prev, [name]: value };
+
+ // Reset model when provider changes
+ if (name === 'provider') {
+ const provider = value as AIProvider;
+ const models = PROVIDER_MODELS[provider];
+ newData.modelId = models?.[0]?.id || '';
+ }
+
+ return newData;
+ });
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ setError(null);
+
+ try {
+ const response = await fetch(`/api/agents/${agentId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ provider: formData.provider,
+ modelId: formData.modelId,
+ systemPrompt: formData.systemPrompt || null,
+ temperature: Number.parseFloat(formData.temperature.toString()),
+ maxToolCallsPerTurn: Number.parseInt(formData.maxToolCallsPerTurn.toString(), 10),
+ maxMessagesInContext: Number.parseInt(formData.maxMessagesInContext.toString(), 10),
+ }),
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ onSettingsChange(formData);
+ onClose();
+ } else {
+ setError(result.error || 'Failed to save settings');
+ }
+ } catch (err) {
+ console.error('Failed to save settings:', err);
+ setError('Failed to save settings');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const models = PROVIDER_MODELS[formData.provider] || [];
+
+ return (
+
+ {/* header */}
+
+
+
+
+
+
+
agent settings
+
{settings.name}
+
+
+
+
+
+ {/* content */}
+
+ {/* model configuration */}
+
+
+ {/* behavior */}
+
+
+ {/* system prompt */}
+
+
+ {/* error message */}
+ {error && (
+
+ )}
+
+
+ {/* footer */}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/agents/ChatToolsPanel.tsx b/apps/web/src/components/agents/ChatToolsPanel.tsx
new file mode 100644
index 0000000..2b17919
--- /dev/null
+++ b/apps/web/src/components/agents/ChatToolsPanel.tsx
@@ -0,0 +1,153 @@
+'use client';
+
+import { Button } from '@tpmjs/ui/Button/Button';
+import { Icon } from '@tpmjs/ui/Icon/Icon';
+
+export interface ToolInfo {
+ id: string;
+ toolId: string;
+ tool: {
+ id: string;
+ name: string;
+ description: string | null;
+ package: {
+ npmPackageName: string;
+ category: string;
+ };
+ };
+}
+
+export interface CollectionInfo {
+ id: string;
+ collectionId: string;
+ collection: {
+ id: string;
+ name: string;
+ toolCount: number;
+ };
+}
+
+interface ChatToolsPanelProps {
+ tools: ToolInfo[];
+ collections: CollectionInfo[];
+ isOpen: boolean;
+ onClose: () => void;
+ onToolClick: (tool: ToolInfo) => void;
+}
+
+export function ChatToolsPanel({
+ tools,
+ collections,
+ isOpen,
+ onClose,
+ onToolClick,
+}: ChatToolsPanelProps) {
+ if (!isOpen) return null;
+
+ const totalToolsFromCollections = collections.reduce(
+ (acc, c) => acc + c.collection.toolCount,
+ 0
+ );
+
+ return (
+
+ {/* header */}
+
+
+ available tools
+
+
+
+
+ {/* tools list */}
+
+ {/* direct tools */}
+ {tools.length > 0 && (
+
+ )}
+
+ {/* collections */}
+ {collections.length > 0 && (
+
+ )}
+
+ {/* empty state */}
+ {tools.length === 0 && collections.length === 0 && (
+
+
+
+
+
No tools attached
+
+ Add tools from the agent settings
+
+
+ )}
+
+
+ {/* footer stats */}
+
+
+
+ {tools.length} direct + {totalToolsFromCollections} from collections
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/agents/ToolDetailsModal.tsx b/apps/web/src/components/agents/ToolDetailsModal.tsx
new file mode 100644
index 0000000..f6fc6ba
--- /dev/null
+++ b/apps/web/src/components/agents/ToolDetailsModal.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import { Badge } from '@tpmjs/ui/Badge/Badge';
+import { Button } from '@tpmjs/ui/Button/Button';
+import { Icon } from '@tpmjs/ui/Icon/Icon';
+import { Modal } from '@tpmjs/ui/Modal/Modal';
+import Link from 'next/link';
+
+import type { ToolInfo } from './ChatToolsPanel';
+
+interface ToolDetailsModalProps {
+ tool: ToolInfo | null;
+ open: boolean;
+ onClose: () => void;
+}
+
+export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps) {
+ if (!tool) return null;
+
+ const toolPageUrl = `/tool/${tool.tool.package.npmPackageName}/${tool.tool.name}`;
+
+ return (
+
+ {/* custom header */}
+
+
+
+
+
+
+
{tool.tool.name}
+
+ {tool.tool.package.npmPackageName}
+
+
+
+
+
+
+ {/* content */}
+
+ {/* overview fieldset */}
+
+
+ {/* note about parameters */}
+
+
+
+ {/* footer */}
+
+
+ id: {tool.tool.id.slice(0, 8)}...
+
+
+
+
+
+
+
+
+
+ );
+}