✨ 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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user