Enhance ActionNode and Clawdbot integration with improved event handling and UI updates

- Added support for new event types in ActionNode, including 'agent' and 'system', with corresponding icons and labels.
- Introduced a formatTime function for consistent timestamp formatting in the UI.
- Updated content truncation logic to handle longer messages more effectively.
- Enhanced session management in MonitorPage to ensure sessions are created for actions and updated based on status changes.
This commit is contained in:
luccast
2026-01-25 21:45:43 -05:00
parent 51b0a3afb9
commit 6f092e724e
4 changed files with 85 additions and 24 deletions
+47 -19
View File
@@ -1,7 +1,15 @@
import { memo, useState } from 'react'
import { Handle, Position } from '@xyflow/react'
import { motion } from 'framer-motion'
import { Loader2, CheckCircle, XCircle, Wrench, MessageSquare } from 'lucide-react'
import {
Loader2,
CheckCircle,
XCircle,
Wrench,
MessageSquare,
MessageCircle,
Bot,
} from 'lucide-react'
import type { MonitorAction } from '~/integrations/clawdbot'
interface ActionNodeProps {
@@ -9,7 +17,15 @@ interface ActionNodeProps {
selected?: boolean
}
const typeConfig: Record<
function formatTime(ts: number): string {
return new Date(ts).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
const stateConfig: Record<
MonitorAction['type'],
{
icon: typeof Loader2
@@ -56,13 +72,21 @@ const typeConfig: Record<
},
}
const eventTypeLabels: Record<MonitorAction['eventType'], { label: string; icon: typeof MessageCircle }> = {
chat: { label: 'Chat', icon: MessageCircle },
agent: { label: 'Agent', icon: Bot },
system: { label: 'System', icon: MessageSquare },
}
export const ActionNode = memo(function ActionNode({
data,
selected,
}: ActionNodeProps) {
const [expanded, setExpanded] = useState(false)
const config = typeConfig[data.type]
const Icon = config.icon
const state = stateConfig[data.type]
const eventInfo = eventTypeLabels[data.eventType || 'chat']
const StateIcon = state.icon
const EventIcon = eventInfo.icon
// Safely get content as string
const contentStr = typeof data.content === 'string'
@@ -72,8 +96,8 @@ export const ActionNode = memo(function ActionNode({
: null
const truncatedContent = contentStr
? contentStr.length > 80
? contentStr.slice(0, 80) + '...'
? contentStr.length > 100
? contentStr.slice(0, 100) + '...'
: contentStr
: null
@@ -86,34 +110,38 @@ export const ActionNode = memo(function ActionNode({
transition={{ duration: 0.2 }}
onClick={() => setExpanded(!expanded)}
className={`
px-3 py-2 rounded-md border min-w-[160px] max-w-[280px] cursor-pointer
${config.color}
px-3 py-2 rounded-md border min-w-[180px] max-w-[300px] cursor-pointer
${state.color}
${selected ? 'ring-2 ring-white/50' : ''}
`}
>
<Handle type="target" position={Position.Top} className="bg-gray-500! w-2! h-2!" />
{/* Header: Event type + state */}
<div className="flex items-center gap-2 mb-1">
<Icon
size={14}
className={`${config.iconColor} ${config.animate ? 'animate-spin' : ''}`}
<EventIcon size={14} className="text-gray-400" />
<span className="text-xs font-medium text-gray-200">
{eventInfo.label} Event
</span>
<StateIcon
size={12}
className={`${state.iconColor} ${state.animate ? 'animate-spin' : ''} ml-auto`}
/>
<span className="text-xs font-medium text-gray-200 capitalize">
{data.type.replace('_', ' ')}
</span>
<span className="text-[10px] text-gray-500 ml-auto">
#{data.seq}
</span>
</div>
{/* Timestamp */}
<div className="text-[10px] text-gray-500 mb-1">
{formatTime(data.timestamp)}
</div>
{data.toolName && (
<div className="text-xs text-purple-300 mb-1 font-mono">
{data.toolName}
🔧 {data.toolName}
</div>
)}
{truncatedContent && (
<div className="text-[11px] text-gray-300 leading-tight">
<div className="text-[11px] text-gray-300 leading-tight whitespace-pre-wrap">
{expanded ? fullContent : truncatedContent}
</div>
)}
+2 -1
View File
@@ -28,6 +28,7 @@ export function chatEventToAction(event: ChatEvent): MonitorAction {
sessionKey: event.sessionKey,
seq: event.seq,
type: event.state,
eventType: 'chat',
timestamp: Date.now(),
}
@@ -102,11 +103,11 @@ export function agentEventToAction(event: AgentEvent): MonitorAction {
sessionKey: event.stream,
seq: event.seq,
type,
eventType: 'agent' as const,
timestamp: event.ts,
content,
toolName,
toolArgs,
parentId: event.seq > 0 ? `${event.runId}-${event.seq - 1}` : undefined,
}
}
+1 -1
View File
@@ -117,11 +117,11 @@ export interface MonitorAction {
sessionKey: string
seq: number
type: 'delta' | 'final' | 'aborted' | 'error' | 'tool_call' | 'tool_result'
eventType: 'chat' | 'agent' | 'system'
timestamp: number
content?: string
toolName?: string
toolArgs?: unknown
parentId?: string
}
// Utility functions
+35 -3
View File
@@ -8,8 +8,9 @@ import {
actionsCollection,
upsertSession,
addAction,
updateSessionStatus,
clearCollections,
parseSessionKey,
type MonitorSession,
} from '~/integrations/clawdbot'
import {
ActionGraph,
@@ -147,11 +148,42 @@ function MonitorPage() {
onData: (data) => {
if (data.type === 'session' && data.session) {
const key = data.session.key
if (key && data.session.status) {
updateSessionStatus(key, data.session.status)
if (key) {
// Create session if it doesn't exist
const existing = sessionsCollection.state.get(key)
if (!existing) {
const parsed = parseSessionKey(key)
const newSession: MonitorSession = {
key,
agentId: parsed.agentId,
platform: parsed.platform,
recipient: parsed.recipient,
isGroup: parsed.isGroup,
lastActivityAt: Date.now(),
status: data.session.status || 'active',
}
upsertSession(newSession)
} else if (data.session.status) {
upsertSession({ ...existing, status: data.session.status, lastActivityAt: Date.now() })
}
}
}
if (data.type === 'action' && data.action) {
// Ensure session exists for this action
const sessionKey = data.action.sessionKey
const existing = sessionsCollection.state.get(sessionKey)
if (!existing) {
const parsed = parseSessionKey(sessionKey)
upsertSession({
key: sessionKey,
agentId: parsed.agentId,
platform: parsed.platform,
recipient: parsed.recipient,
isGroup: parsed.isGroup,
lastActivityAt: Date.now(),
status: 'active',
})
}
addAction(data.action)
}
},