feat: implement session-based message isolation and improve agent creation UX

- Add sessionId field to PocMessage interface for session isolation
- Filter messages by current session to prevent cross-session pollution
- Remove automatic agent creation - only manual creation by users
- Add beautiful empty state when no agents exist
- Enhance sidebar UX with prominent "Create Agent" button when empty
- Update message hooks to handle session-specific messaging
- Improve user control over agent lifecycle management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vaayne
2025-07-31 23:53:41 +08:00
parent dc603d9896
commit 571f6c3ef3
3 changed files with 138 additions and 71 deletions
+27 -12
View File
@@ -15,31 +15,32 @@ const logger = loggerService.withContext('UsePocMessages')
* - Handles message completion status
* - Real-time updates from AgentCommandService events
*/
export function usePocMessages() {
export function usePocMessages(currentSessionId?: string) {
const [messages, setMessages] = useState<PocMessage[]>([])
/**
* Adds a user command message to the list
*/
const addUserCommand = useCallback((command: string, commandId: string) => {
const addUserCommand = useCallback((command: string, commandId: string, sessionId?: string) => {
const message: PocMessage = {
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: 'user-command',
content: command,
timestamp: Date.now(),
commandId,
sessionId,
isComplete: true
}
setMessages((prev) => [...prev, message])
logger.debug('Added user command message', { commandId, command: command.substring(0, 50) })
logger.debug('Added user command message', { commandId, sessionId, command: command.substring(0, 50) })
}, [])
/**
* Appends streaming output to existing message or creates new one
*/
const appendOutput = useCallback(
(commandId: string, content: string, type: 'output' | 'error' = 'output', isComplete: boolean = false) => {
(commandId: string, content: string, type: 'output' | 'error' = 'output', isComplete: boolean = false, sessionId?: string) => {
setMessages((prev) => {
// Find existing output message for this command
const existingIndex = prev.findIndex(
@@ -64,6 +65,7 @@ export function usePocMessages() {
content,
timestamp: Date.now(),
commandId,
sessionId,
isComplete
}
return [...prev, message]
@@ -74,7 +76,8 @@ export function usePocMessages() {
commandId,
type,
contentLength: content.length,
isComplete
isComplete,
sessionId
})
},
[]
@@ -95,18 +98,19 @@ export function usePocMessages() {
/**
* Adds a system message (e.g., command completion notifications)
*/
const addSystemMessage = useCallback((content: string, commandId?: string) => {
const addSystemMessage = useCallback((content: string, commandId?: string, sessionId?: string) => {
const message: PocMessage = {
id: `system_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: 'system',
content,
timestamp: Date.now(),
commandId,
sessionId,
isComplete: true
}
setMessages((prev) => [...prev, message])
logger.debug('Added system message', { content, commandId })
logger.debug('Added system message', { content, commandId, sessionId })
}, [])
/**
@@ -127,6 +131,16 @@ export function usePocMessages() {
[messages]
)
/**
* Gets messages for a specific session
*/
const getMessagesForSession = useCallback(
(sessionId: string): PocMessage[] => {
return messages.filter((msg) => msg.sessionId === sessionId)
},
[messages]
)
/**
* Gets the latest incomplete output message for a command
*/
@@ -147,7 +161,7 @@ export function usePocMessages() {
if (type === 'stdout' || type === 'stderr') {
const messageType = type === 'stderr' ? 'error' : 'output'
appendOutput(commandId, data, messageType, false)
appendOutput(commandId, data, messageType, false, currentSessionId)
} else if (type === 'exit') {
// Complete any streaming messages when command exits
completeMessage(commandId, 'output')
@@ -156,15 +170,15 @@ export function usePocMessages() {
// Add system message for command completion
const exitCode = output.exitCode ?? 0
const statusMessage = exitCode === 0 ? 'Command completed successfully' : `Command exited with code ${exitCode}`
addSystemMessage(statusMessage, commandId)
addSystemMessage(statusMessage, commandId, currentSessionId)
} else if (type === 'error') {
appendOutput(commandId, data, 'error', true)
appendOutput(commandId, data, 'error', true, currentSessionId)
}
})
// Handle command errors
const unsubscribeError = agentCommandService.on('commandError', ({ commandId, error }) => {
addSystemMessage(`Command error: ${error}`, commandId)
addSystemMessage(`Command error: ${error}`, commandId, currentSessionId)
})
// Cleanup on unmount
@@ -173,7 +187,7 @@ export function usePocMessages() {
unsubscribeOutput()
unsubscribeError()
}
}, [appendOutput, completeMessage, addSystemMessage])
}, [appendOutput, completeMessage, addSystemMessage, currentSessionId])
return {
messages,
@@ -183,6 +197,7 @@ export function usePocMessages() {
addSystemMessage,
clearMessages,
getMessagesForCommand,
getMessagesForSession,
getStreamingMessage
}
}
@@ -52,10 +52,10 @@ const CherryAgentPage: React.FC = () => {
const { isLeftNavbar } = useNavbarPosition()
// Initialize hooks
const messagesHook = usePocMessages()
const agentManagement = useAgentManagement()
const messagesHook = usePocMessages(agentManagement.currentSession?.id)
const commandHook = usePocCommand()
const historyHook = useCommandHistory()
const agentManagement = useAgentManagement()
// Local state for command count tracking
const [commandCount, setCommandCount] = useState(0)
@@ -80,7 +80,7 @@ const CherryAgentPage: React.FC = () => {
const commandId = await commandHook.executeCommand(command, workingDirectory)
if (commandId) {
// Add user command message
messagesHook.addUserCommand(command, commandId)
messagesHook.addUserCommand(command, commandId, agentManagement.currentSession?.id)
setCommandCount((prev) => prev + 1)
}
},
@@ -94,10 +94,10 @@ const CherryAgentPage: React.FC = () => {
// Note: The type determination (stdout vs stderr) is handled in usePocMessages
// based on the command output type from AgentCommandService
if (isBuffered && output) {
messagesHook.appendOutput(commandId, output, 'output', false)
messagesHook.appendOutput(commandId, output, 'output', false, agentManagement.currentSession?.id)
}
})
}, [commandHook, messagesHook])
}, [commandHook, messagesHook, agentManagement.currentSession?.id])
// Determine current status for status bar
const getCommandStatus = useCallback(() => {
@@ -267,10 +267,10 @@ const CherryAgentPage: React.FC = () => {
if (commandHook.currentCommandId) {
const success = await commandHook.interruptCommand(commandHook.currentCommandId)
if (success) {
messagesHook.appendOutput(commandHook.currentCommandId, '\n[Command cancelled by user]', 'error', true)
messagesHook.appendOutput(commandHook.currentCommandId, '\n[Command cancelled by user]', 'error', true, agentManagement.currentSession?.id)
}
}
}, [commandHook, messagesHook])
}, [commandHook, messagesHook, agentManagement.currentSession?.id])
const toggleSidebar = useCallback(() => {
setSidebarCollapsed((prev) => !prev)
@@ -302,17 +302,6 @@ const CherryAgentPage: React.FC = () => {
(session) => agentManagement.currentAgent && session.agent_ids.includes(agentManagement.currentAgent.id)
)
// Create a default agent if none exists
useEffect(() => {
if (!agentManagement.loadingAgents && agentManagement.agents.length === 0) {
agentManagement.createAgent({
name: 'Assistant',
model: '',
description: 'General-purpose AI assistant',
instructions: 'You are a helpful assistant that can execute commands and help with tasks.'
})
}
}, [agentManagement])
// Set the first agent as current if none is selected
useEffect(() => {
@@ -350,13 +339,24 @@ const CherryAgentPage: React.FC = () => {
<SidebarHeader>
<HeaderLabel>agents</HeaderLabel>
<HeaderActions>
<ActionButton
type="text"
icon={<PlusOutlined />}
size="small"
title="Agent Actions"
onClick={handleCreateAgent}
/>
{agentManagement.agents.length === 0 ? (
<Button
type="primary"
icon={<PlusOutlined />}
size="small"
onClick={handleCreateAgent}
>
Create Agent
</Button>
) : (
<ActionButton
type="text"
icon={<PlusOutlined />}
size="small"
title="Create New Agent"
onClick={handleCreateAgent}
/>
)}
<CollapseButton type="text" icon={<MenuFoldOutlined />} onClick={toggleSidebar} size="small" />
</HeaderActions>
</SidebarHeader>
@@ -461,42 +461,64 @@ const CherryAgentPage: React.FC = () => {
{/* Main Content Area */}
<MainContent>
{/* Agent Info Header */}
<AgentHeader>
{sidebarCollapsed && (
<ToggleButton type="text" icon={<MenuUnfoldOutlined />} onClick={toggleSidebar} size="small" />
)}
{agentManagement.currentAgent && (
<CurrentAgentAvatar style={{ background: getAvatarGradient(agentManagement.currentAgent.name) }}>
{agentManagement.currentAgent.name.charAt(0).toUpperCase()}
</CurrentAgentAvatar>
)}
<AgentTitleSection>
<AgentNameInput
value={agentManagement.currentAgent?.name || ''}
onChange={(e) => handleAgentNameChange(e.target.value)}
onBlur={(e) => handleAgentNameChange(e.target.value)}
placeholder="Agent Name"
/>
<AgentSubtitle>{agentManagement.currentAgent?.description || 'No description provided'}</AgentSubtitle>
</AgentTitleSection>
</AgentHeader>
{agentManagement.currentAgent ? (
<>
{/* Agent Info Header */}
<AgentHeader>
{sidebarCollapsed && (
<ToggleButton type="text" icon={<MenuUnfoldOutlined />} onClick={toggleSidebar} size="small" />
)}
<CurrentAgentAvatar style={{ background: getAvatarGradient(agentManagement.currentAgent.name) }}>
{agentManagement.currentAgent.name.charAt(0).toUpperCase()}
</CurrentAgentAvatar>
<AgentTitleSection>
<AgentNameInput
value={agentManagement.currentAgent.name}
onChange={(e) => handleAgentNameChange(e.target.value)}
onBlur={(e) => handleAgentNameChange(e.target.value)}
placeholder="Agent Name"
/>
<AgentSubtitle>{agentManagement.currentAgent.description || 'No description provided'}</AgentSubtitle>
</AgentTitleSection>
</AgentHeader>
<MessageArea>
<PocMessageList messages={messagesHook.messages} />
</MessageArea>
<MessageArea>
<PocMessageList
messages={agentManagement.currentSession
? messagesHook.getMessagesForSession(agentManagement.currentSession.id)
: []
}
/>
</MessageArea>
<InputArea>
<EnhancedCommandInput
status={getCommandStatus()}
activeCommand={getCurrentCommand()}
commandCount={commandCount}
onSendCommand={handleExecuteCommand}
onCancelCommand={commandHook.isExecuting ? handleCancelCommand : undefined}
onOpenSettings={handleOpenSettings}
disabled={commandHook.isExecuting}
/>
</InputArea>
<InputArea>
<EnhancedCommandInput
status={getCommandStatus()}
activeCommand={getCurrentCommand()}
commandCount={commandCount}
onSendCommand={handleExecuteCommand}
onCancelCommand={commandHook.isExecuting ? handleCancelCommand : undefined}
onOpenSettings={handleOpenSettings}
disabled={commandHook.isExecuting}
/>
</InputArea>
</>
) : (
<EmptyAgentState>
{sidebarCollapsed && (
<ToggleButton type="text" icon={<MenuUnfoldOutlined />} onClick={toggleSidebar} size="small" style={{ position: 'absolute', top: '20px', left: '20px' }} />
)}
<EmptyStateContent>
<EmptyStateTitle>No Agent Selected</EmptyStateTitle>
<EmptyStateDescription>
Create an agent to get started with Cherry Agent. You can create different agents with specific roles and capabilities.
</EmptyStateDescription>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateAgent} size="large">
Create Your First Agent
</Button>
</EmptyStateContent>
</EmptyAgentState>
)}
</MainContent>
</ContentContainer>
<CherryAgentSettingsModal
@@ -779,6 +801,35 @@ const EmptyState = styled.div`
font-style: italic;
`
const EmptyAgentState = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
background: linear-gradient(135deg, var(--color-background) 0%, var(--color-background-soft) 100%);
`
const EmptyStateContent = styled.div`
text-align: center;
max-width: 400px;
padding: 40px;
`
const EmptyStateTitle = styled.h2`
font-size: 24px;
font-weight: 600;
color: var(--color-text);
margin: 0 0 16px 0;
`
const EmptyStateDescription = styled.p`
font-size: 16px;
color: var(--color-text-secondary);
line-height: 1.5;
margin: 0 0 32px 0;
`
const AgentTitleSection = styled.div`
flex: 1;
display: flex;
+1
View File
@@ -6,6 +6,7 @@ export interface PocMessage {
content: string
timestamp: number
commandId?: string // Links output to originating command
sessionId?: string // Links message to specific session
isComplete: boolean // For streaming messages
}