From 3228a8e4171be661c41f10f70dd2e89a512c45cf Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 30 Oct 2025 03:33:13 +0000 Subject: [PATCH 01/15] feat: Replace homepage with conversation manager using TypeScript SDK - Remove most homepage content, keeping only settings button - Add ConversationManager React component with full CRUD functionality - Implement conversation listing, creation, messaging, and deletion - Display latest event and current state for each conversation - Add ConversationManager class to SDK for API operations - Export new types: ConversationSearchRequest/Response, ConversationManagerOptions - Update example app to showcase conversation management capabilities Co-authored-by: openhands --- example/src/App.tsx | 101 +----- .../src/components/ConversationManager.css | 307 ++++++++++++++++ .../src/components/ConversationManager.tsx | 333 ++++++++++++++++++ example/vite.config.ts | 4 +- src/conversation/conversation-manager.ts | 117 ++++++ src/index.ts | 7 + src/models/conversation.ts | 13 + 7 files changed, 782 insertions(+), 100 deletions(-) create mode 100644 example/src/components/ConversationManager.css create mode 100644 example/src/components/ConversationManager.tsx create mode 100644 src/conversation/conversation-manager.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index 135d7eb..d5240a0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,62 +1,19 @@ -import { useState, useEffect } from 'react' import './App.css' -// Import the OpenHands SDK -import { - RemoteConversation, - RemoteWorkspace, - HttpClient, - AgentExecutionStatus, - EventSortOrder -} from '@openhands/agent-server-typescript-client' - // Import settings components import { SettingsModal } from './components/SettingsModal' +import { ConversationManager } from './components/ConversationManager' import { useSettings } from './contexts/SettingsContext' function App() { - const [sdkStatus, setSdkStatus] = useState('Loading...') - const [sdkInfo, setSdkInfo] = useState(null) - // Use settings context const { settings, updateSettings, isModalOpen, openModal, closeModal, isFirstVisit } = useSettings() - useEffect(() => { - // Test that the SDK imports work correctly - try { - // Check that all main classes are available - const classes = { - RemoteConversation: typeof RemoteConversation, - RemoteWorkspace: typeof RemoteWorkspace, - HttpClient: typeof HttpClient, - AgentExecutionStatus: typeof AgentExecutionStatus, - EventSortOrder: typeof EventSortOrder, - } - - // Check that enums have expected values - const enumValues = { - AgentExecutionStatus: Object.keys(AgentExecutionStatus), - EventSortOrder: Object.keys(EventSortOrder), - } - - setSdkInfo({ - classes, - enumValues, - importTime: new Date().toISOString(), - }) - - setSdkStatus('✅ SDK imported successfully!') - } catch (error) { - setSdkStatus(`❌ SDK import failed: ${error}`) - console.error('SDK import error:', error) - } - }, []) - return (
-

OpenHands SDK Example

+

OpenHands Conversation Manager

@@ -68,59 +25,7 @@ function App() {
)} -
-

SDK Import Status

-

{sdkStatus}

- - {sdkInfo && ( -
-

Available Classes:

-
    - {Object.entries(sdkInfo.classes).map(([name, type]) => ( -
  • - {name}: {type as string} -
  • - ))} -
- -

Enum Values:

-
-
- AgentExecutionStatus: -
    - {sdkInfo.enumValues.AgentExecutionStatus.map((value: string) => ( -
  • {value}
  • - ))} -
-
-
- EventSortOrder: -
    - {sdkInfo.enumValues.EventSortOrder.map((value: string) => ( -
  • {value}
  • - ))} -
-
-
- -

- Imported at: {sdkInfo.importTime} -

-
- )} -
- -
-

Hello World from React + TypeScript + OpenHands SDK!

-

- This is a basic React application that successfully imports and uses the - OpenHands Agent Server TypeScript Client SDK. -

-

- The SDK is built locally and linked as a file dependency, demonstrating - that the build process works correctly. -

-
+ { + const { settings } = useSettings(); + const [conversations, setConversations] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [manager, setManager] = useState(null); + const [selectedConversation, setSelectedConversation] = useState(null); + const [messageInput, setMessageInput] = useState(''); + const [activeConversations, setActiveConversations] = useState>(new Map()); + + // Initialize conversation manager + useEffect(() => { + if (settings.agentServerUrl) { + const newManager = new SDKConversationManager({ + host: settings.agentServerUrl, + apiKey: settings.agentServerApiKey || undefined, + }); + setManager(newManager); + + return () => { + newManager.close(); + }; + } + }, [settings.agentServerUrl, settings.agentServerApiKey]); + + // Load conversations + const loadConversations = async () => { + if (!manager) return; + + setLoading(true); + setError(null); + try { + const conversationList = await manager.getAllConversations(); + setConversations(conversationList.map(conv => ({ ...conv, isLoading: false }))); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load conversations'); + } finally { + setLoading(false); + } + }; + + // Load conversations when manager is ready + useEffect(() => { + if (manager) { + loadConversations(); + } + }, [manager]); + + // Create new conversation + const createConversation = async () => { + if (!manager) return; + + setLoading(true); + setError(null); + try { + const agent: AgentBase = { + name: 'CodeActAgent', + llm: { + model: settings.modelName, + api_key: settings.apiKey, + }, + }; + + const conversation = await manager.createConversation(agent, { + initialMessage: 'Hello! I\'m ready to help you with your tasks.', + maxIterations: 50, + stuckDetection: true, + }); + + // Add to active conversations + setActiveConversations(prev => new Map(prev.set(conversation.id, conversation))); + + // Reload conversations list + await loadConversations(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create conversation'); + } finally { + setLoading(false); + } + }; + + // Delete conversation + const deleteConversation = async (conversationId: string) => { + if (!manager) return; + + if (!confirm('Are you sure you want to delete this conversation?')) { + return; + } + + setLoading(true); + setError(null); + try { + await manager.deleteConversation(conversationId); + + // Remove from active conversations + const activeConv = activeConversations.get(conversationId); + if (activeConv) { + await activeConv.close(); + setActiveConversations(prev => { + const newMap = new Map(prev); + newMap.delete(conversationId); + return newMap; + }); + } + + // Clear selection if this conversation was selected + if (selectedConversation === conversationId) { + setSelectedConversation(null); + } + + // Reload conversations list + await loadConversations(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete conversation'); + } finally { + setLoading(false); + } + }; + + // Send message to conversation + const sendMessage = async (conversationId: string, message: string) => { + if (!manager || !message.trim()) return; + + setError(null); + try { + let conversation = activeConversations.get(conversationId); + + if (!conversation) { + // Load the conversation if not already active + conversation = await manager.loadConversation(conversationId); + setActiveConversations(prev => new Map(prev.set(conversationId, conversation!))); + } + + await conversation.sendMessage(message); + await conversation.run(); + + setMessageInput(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to send message'); + } + }; + + // Get status color + const getStatusColor = (status: AgentExecutionStatus): string => { + switch (status) { + case AgentExecutionStatus.IDLE: + return '#6b7280'; + case AgentExecutionStatus.RUNNING: + return '#3b82f6'; + case AgentExecutionStatus.PAUSED: + return '#f59e0b'; + case AgentExecutionStatus.FINISHED: + return '#10b981'; + case AgentExecutionStatus.ERROR: + return '#ef4444'; + default: + return '#6b7280'; + } + }; + + // Format timestamp (for future use) + // const formatTimestamp = (timestamp: string): string => { + // return new Date(timestamp).toLocaleString(); + // }; + + if (!settings.agentServerUrl || !settings.apiKey) { + return ( +
+
+

Configuration Required

+

Please configure your settings to start using the conversation manager.

+
+
+ ); + } + + return ( +
+
+

Conversation Manager

+
+ + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + {loading && ( +
+ Loading conversations... +
+ )} + +
+
+

Conversations ({conversations.length})

+ {conversations.length === 0 && !loading ? ( +
+

No conversations yet. Create your first conversation!

+
+ ) : ( +
+ {conversations.map((conversation) => ( +
setSelectedConversation(conversation.id)} + > +
+
+ ID: {conversation.id.substring(0, 8)}... +
+
+ + Status: {conversation.agent_status} +
+
+ Events: {conversation.conversation_stats?.total_events || 0} | + Messages: {conversation.conversation_stats?.message_events || 0} +
+
+ Agent: {conversation.agent?.name || 'Unknown'} +
+
+
+ +
+
+ ))} +
+ )} +
+ + {selectedConversation && ( +
+

Conversation Details

+
+ {(() => { + const conv = conversations.find(c => c.id === selectedConversation); + if (!conv) return null; + + return ( +
+

ID: {conv.id}

+

Status: + + {conv.agent_status} +

+

Agent: {conv.agent?.name}

+

Model: {conv.agent?.llm?.model}

+

Total Events: {conv.conversation_stats?.total_events || 0}

+

Messages: {conv.conversation_stats?.message_events || 0}

+

Actions: {conv.conversation_stats?.action_events || 0}

+

Observations: {conv.conversation_stats?.observation_events || 0}

+
+ ); + })()} +
+ +
+

Send Message

+
+