Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Henri Bonamy commited on
Commit ·
72af187
1
Parent(s): 35dc01a
improved tool calling, push to hub, etc.
Browse files- frontend/src/components/Chat/ApprovalFlow.tsx +10 -0
- frontend/src/components/Chat/MessageBubble.tsx +50 -29
- frontend/src/components/Chat/MessageList.tsx +2 -43
- frontend/src/components/SessionSidebar/SessionSidebar.tsx +9 -3
- frontend/src/hooks/useAgentWebSocket.ts +60 -35
- frontend/src/store/agentStore.ts +41 -0
- frontend/src/types/agent.ts +1 -0
frontend/src/components/Chat/ApprovalFlow.tsx
CHANGED
|
@@ -118,6 +118,10 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
|
|
| 118 |
|
| 119 |
const currentTool = batch.tools[currentIndex];
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
const getToolDescription = (toolName: string, args: any) => {
|
| 122 |
if (toolName === 'hf_jobs') {
|
| 123 |
return (
|
|
@@ -217,6 +221,12 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
|
|
| 217 |
<OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
|
| 218 |
</Box>
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
{showLogsButton && (
|
| 221 |
<Button
|
| 222 |
variant="outlined"
|
|
|
|
| 118 |
|
| 119 |
const currentTool = batch.tools[currentIndex];
|
| 120 |
|
| 121 |
+
// Check if script contains push_to_hub or upload_file
|
| 122 |
+
const args = currentTool.arguments as any;
|
| 123 |
+
const containsPushToHub = currentTool.tool === 'hf_jobs' && args.script && (args.script.includes('push_to_hub') || args.script.includes('upload_file'));
|
| 124 |
+
|
| 125 |
const getToolDescription = (toolName: string, args: any) => {
|
| 126 |
if (toolName === 'hf_jobs') {
|
| 127 |
return (
|
|
|
|
| 221 |
<OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
|
| 222 |
</Box>
|
| 223 |
|
| 224 |
+
{containsPushToHub && (
|
| 225 |
+
<Typography variant="caption" sx={{ color: 'var(--accent-green)', fontSize: '0.75rem', opacity: 0.8, px: 0.5 }}>
|
| 226 |
+
We've detected the result will be pushed to hub.
|
| 227 |
+
</Typography>
|
| 228 |
+
)}
|
| 229 |
+
|
| 230 |
{showLogsButton && (
|
| 231 |
<Button
|
| 232 |
variant="outlined"
|
frontend/src/components/Chat/MessageBubble.tsx
CHANGED
|
@@ -63,34 +63,6 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
|
|
| 63 |
</Box>
|
| 64 |
)}
|
| 65 |
|
| 66 |
-
{/* Persisted Trace Logs */}
|
| 67 |
-
{message.trace && message.trace.length > 0 && (
|
| 68 |
-
<Box
|
| 69 |
-
sx={{
|
| 70 |
-
bgcolor: 'rgba(0,0,0,0.3)',
|
| 71 |
-
borderRadius: 1,
|
| 72 |
-
p: 1.5,
|
| 73 |
-
border: 1,
|
| 74 |
-
borderColor: 'rgba(255,255,255,0.05)',
|
| 75 |
-
width: '100%',
|
| 76 |
-
mb: 2,
|
| 77 |
-
}}
|
| 78 |
-
>
|
| 79 |
-
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
| 80 |
-
{message.trace.map((log) => (
|
| 81 |
-
<Typography
|
| 82 |
-
key={log.id}
|
| 83 |
-
variant="caption"
|
| 84 |
-
component="div"
|
| 85 |
-
sx={{ color: 'var(--muted-text)', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace', fontSize: '0.75rem' }}
|
| 86 |
-
>
|
| 87 |
-
> {log.text}
|
| 88 |
-
</Typography>
|
| 89 |
-
))}
|
| 90 |
-
</Box>
|
| 91 |
-
</Box>
|
| 92 |
-
)}
|
| 93 |
-
|
| 94 |
<Box
|
| 95 |
sx={{
|
| 96 |
'& p': { m: 0, color: isUser ? 'var(--text)' : 'var(--text)' }, // User might want different text color? Defaults to --text
|
|
@@ -147,7 +119,56 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
|
|
| 147 |
>
|
| 148 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
| 149 |
</Box>
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
<Typography className="meta" variant="caption" sx={{ display: 'block', textAlign: 'right', mt: 1, fontSize: '11px', opacity: 0.5 }}>
|
| 152 |
{new Date(message.timestamp).toLocaleTimeString()}
|
| 153 |
</Typography>
|
|
|
|
| 63 |
</Box>
|
| 64 |
)}
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
<Box
|
| 67 |
sx={{
|
| 68 |
'& p': { m: 0, color: isUser ? 'var(--text)' : 'var(--text)' }, // User might want different text color? Defaults to --text
|
|
|
|
| 119 |
>
|
| 120 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
| 121 |
</Box>
|
| 122 |
+
|
| 123 |
+
{/* Persisted Trace Logs - Now at the bottom */}
|
| 124 |
+
{message.trace && message.trace.length > 0 && (
|
| 125 |
+
<Box
|
| 126 |
+
sx={{
|
| 127 |
+
bgcolor: 'rgba(0,0,0,0.3)',
|
| 128 |
+
borderRadius: 1,
|
| 129 |
+
p: 1.5,
|
| 130 |
+
border: 1,
|
| 131 |
+
borderColor: 'rgba(255,255,255,0.05)',
|
| 132 |
+
width: '100%',
|
| 133 |
+
mt: 2,
|
| 134 |
+
}}
|
| 135 |
+
>
|
| 136 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
| 137 |
+
{message.trace.map((log) => {
|
| 138 |
+
// Extract tool name from text "Agent is executing {toolName}..."
|
| 139 |
+
const match = log.text.match(/Agent is executing (.+)\.\.\./);
|
| 140 |
+
const toolName = match ? match[1] : log.tool;
|
| 141 |
+
|
| 142 |
+
return (
|
| 143 |
+
<Typography
|
| 144 |
+
key={log.id}
|
| 145 |
+
variant="caption"
|
| 146 |
+
component="div"
|
| 147 |
+
sx={{
|
| 148 |
+
color: 'var(--muted-text)',
|
| 149 |
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
|
| 150 |
+
fontSize: '0.75rem',
|
| 151 |
+
display: 'flex',
|
| 152 |
+
alignItems: 'center',
|
| 153 |
+
gap: 0.5,
|
| 154 |
+
}}
|
| 155 |
+
>
|
| 156 |
+
<span style={{ color: log.completed ? '#FDB022' : 'inherit' }}>*</span>
|
| 157 |
+
<span>Agent is executing </span>
|
| 158 |
+
<span style={{
|
| 159 |
+
fontWeight: 600,
|
| 160 |
+
color: 'rgba(255, 255, 255, 0.9)',
|
| 161 |
+
}}>
|
| 162 |
+
{toolName}
|
| 163 |
+
</span>
|
| 164 |
+
<span>...</span>
|
| 165 |
+
</Typography>
|
| 166 |
+
);
|
| 167 |
+
})}
|
| 168 |
+
</Box>
|
| 169 |
+
</Box>
|
| 170 |
+
)}
|
| 171 |
+
|
| 172 |
<Typography className="meta" variant="caption" sx={{ display: 'block', textAlign: 'right', mt: 1, fontSize: '11px', opacity: 0.5 }}>
|
| 173 |
{new Date(message.timestamp).toLocaleTimeString()}
|
| 174 |
</Typography>
|
frontend/src/components/Chat/MessageList.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import { useEffect, useRef } from 'react';
|
| 2 |
import { Box, Typography } from '@mui/material';
|
| 3 |
-
import { useAgentStore } from '@/store/agentStore';
|
| 4 |
import { useSessionStore } from '@/store/sessionStore';
|
| 5 |
import MessageBubble from './MessageBubble';
|
| 6 |
import type { Message } from '@/types/agent';
|
|
@@ -40,21 +39,12 @@ const TechnicalIndicator = () => (
|
|
| 40 |
|
| 41 |
export default function MessageList({ messages, isProcessing }: MessageListProps) {
|
| 42 |
const bottomRef = useRef<HTMLDivElement>(null);
|
| 43 |
-
const traceBoxRef = useRef<HTMLDivElement>(null);
|
| 44 |
-
const { traceLogs } = useAgentStore();
|
| 45 |
const { activeSessionId } = useSessionStore();
|
| 46 |
|
| 47 |
// Auto-scroll to bottom when new messages arrive
|
| 48 |
useEffect(() => {
|
| 49 |
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 50 |
-
}, [messages, isProcessing
|
| 51 |
-
|
| 52 |
-
// Auto-scroll trace box
|
| 53 |
-
useEffect(() => {
|
| 54 |
-
if (traceBoxRef.current) {
|
| 55 |
-
traceBoxRef.current.scrollTop = traceBoxRef.current.scrollHeight;
|
| 56 |
-
}
|
| 57 |
-
}, [traceLogs]);
|
| 58 |
|
| 59 |
return (
|
| 60 |
<Box
|
|
@@ -67,7 +57,7 @@ export default function MessageList({ messages, isProcessing }: MessageListProps
|
|
| 67 |
}}
|
| 68 |
>
|
| 69 |
<Box sx={{ maxWidth: 'md', mx: 'auto', width: '100%', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 70 |
-
{messages.length === 0 &&
|
| 71 |
<Box
|
| 72 |
sx={{
|
| 73 |
flex: 1,
|
|
@@ -95,37 +85,6 @@ export default function MessageList({ messages, isProcessing }: MessageListProps
|
|
| 95 |
</Typography>
|
| 96 |
<TechnicalIndicator />
|
| 97 |
</Box>
|
| 98 |
-
|
| 99 |
-
{traceLogs.length > 0 && (
|
| 100 |
-
<Box
|
| 101 |
-
sx={{
|
| 102 |
-
bgcolor: 'background.default',
|
| 103 |
-
borderRadius: 1,
|
| 104 |
-
p: 2,
|
| 105 |
-
border: 1,
|
| 106 |
-
borderColor: 'divider',
|
| 107 |
-
width: '100%',
|
| 108 |
-
fontFamily: 'monospace',
|
| 109 |
-
maxHeight: 120,
|
| 110 |
-
overflowY: 'auto',
|
| 111 |
-
}}
|
| 112 |
-
ref={traceBoxRef}
|
| 113 |
-
>
|
| 114 |
-
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
| 115 |
-
{traceLogs.map((log) => (
|
| 116 |
-
<Box key={log.id}>
|
| 117 |
-
<Typography
|
| 118 |
-
variant="caption"
|
| 119 |
-
component="div"
|
| 120 |
-
sx={{ color: 'common.white', fontFamily: 'monospace' }}
|
| 121 |
-
>
|
| 122 |
-
> {log.text}
|
| 123 |
-
</Typography>
|
| 124 |
-
</Box>
|
| 125 |
-
))}
|
| 126 |
-
</Box>
|
| 127 |
-
</Box>
|
| 128 |
-
)}
|
| 129 |
</Box>
|
| 130 |
)}
|
| 131 |
|
|
|
|
| 1 |
import { useEffect, useRef } from 'react';
|
| 2 |
import { Box, Typography } from '@mui/material';
|
|
|
|
| 3 |
import { useSessionStore } from '@/store/sessionStore';
|
| 4 |
import MessageBubble from './MessageBubble';
|
| 5 |
import type { Message } from '@/types/agent';
|
|
|
|
| 39 |
|
| 40 |
export default function MessageList({ messages, isProcessing }: MessageListProps) {
|
| 41 |
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
| 42 |
const { activeSessionId } = useSessionStore();
|
| 43 |
|
| 44 |
// Auto-scroll to bottom when new messages arrive
|
| 45 |
useEffect(() => {
|
| 46 |
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 47 |
+
}, [messages, isProcessing]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
return (
|
| 50 |
<Box
|
|
|
|
| 57 |
}}
|
| 58 |
>
|
| 59 |
<Box sx={{ maxWidth: 'md', mx: 'auto', width: '100%', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 60 |
+
{messages.length === 0 && !isProcessing ? (
|
| 61 |
<Box
|
| 62 |
sx={{
|
| 63 |
flex: 1,
|
|
|
|
| 85 |
</Typography>
|
| 86 |
<TechnicalIndicator />
|
| 87 |
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</Box>
|
| 89 |
)}
|
| 90 |
|
frontend/src/components/SessionSidebar/SessionSidebar.tsx
CHANGED
|
@@ -47,18 +47,21 @@ const RunningIndicator = () => (
|
|
| 47 |
export default function SessionSidebar({ onClose }: SessionSidebarProps) {
|
| 48 |
const { sessions, activeSessionId, createSession, deleteSession, switchSession } =
|
| 49 |
useSessionStore();
|
| 50 |
-
const { clearMessages, isConnected, isProcessing } = useAgentStore();
|
| 51 |
|
| 52 |
const handleNewSession = useCallback(async () => {
|
| 53 |
try {
|
| 54 |
const response = await fetch('/api/session', { method: 'POST' });
|
| 55 |
const data = await response.json();
|
| 56 |
createSession(data.session_id);
|
|
|
|
|
|
|
|
|
|
| 57 |
onClose?.();
|
| 58 |
} catch (e) {
|
| 59 |
console.error('Failed to create session:', e);
|
| 60 |
}
|
| 61 |
-
}, [createSession, onClose]);
|
| 62 |
|
| 63 |
const handleDeleteSession = useCallback(
|
| 64 |
async (sessionId: string, e: React.MouseEvent) => {
|
|
@@ -77,9 +80,12 @@ export default function SessionSidebar({ onClose }: SessionSidebarProps) {
|
|
| 77 |
const handleSelectSession = useCallback(
|
| 78 |
(sessionId: string) => {
|
| 79 |
switchSession(sessionId);
|
|
|
|
|
|
|
|
|
|
| 80 |
onClose?.();
|
| 81 |
},
|
| 82 |
-
[switchSession, onClose]
|
| 83 |
);
|
| 84 |
|
| 85 |
const handleUndo = useCallback(async () => {
|
|
|
|
| 47 |
export default function SessionSidebar({ onClose }: SessionSidebarProps) {
|
| 48 |
const { sessions, activeSessionId, createSession, deleteSession, switchSession } =
|
| 49 |
useSessionStore();
|
| 50 |
+
const { clearMessages, isConnected, isProcessing, setPlan, setPanelContent } = useAgentStore();
|
| 51 |
|
| 52 |
const handleNewSession = useCallback(async () => {
|
| 53 |
try {
|
| 54 |
const response = await fetch('/api/session', { method: 'POST' });
|
| 55 |
const data = await response.json();
|
| 56 |
createSession(data.session_id);
|
| 57 |
+
// Clear plan and code panel for new session
|
| 58 |
+
setPlan([]);
|
| 59 |
+
setPanelContent(null);
|
| 60 |
onClose?.();
|
| 61 |
} catch (e) {
|
| 62 |
console.error('Failed to create session:', e);
|
| 63 |
}
|
| 64 |
+
}, [createSession, setPlan, setPanelContent, onClose]);
|
| 65 |
|
| 66 |
const handleDeleteSession = useCallback(
|
| 67 |
async (sessionId: string, e: React.MouseEvent) => {
|
|
|
|
| 80 |
const handleSelectSession = useCallback(
|
| 81 |
(sessionId: string) => {
|
| 82 |
switchSession(sessionId);
|
| 83 |
+
// Clear plan and code panel when switching sessions
|
| 84 |
+
setPlan([]);
|
| 85 |
+
setPanelContent(null);
|
| 86 |
onClose?.();
|
| 87 |
},
|
| 88 |
+
[switchSession, setPlan, setPanelContent, onClose]
|
| 89 |
);
|
| 90 |
|
| 91 |
const handleUndo = useCallback(async () => {
|
frontend/src/hooks/useAgentWebSocket.ts
CHANGED
|
@@ -25,15 +25,19 @@ export function useAgentWebSocket({
|
|
| 25 |
|
| 26 |
const {
|
| 27 |
addMessage,
|
|
|
|
| 28 |
setProcessing,
|
| 29 |
setConnected,
|
| 30 |
setPendingApprovals,
|
| 31 |
setError,
|
| 32 |
addTraceLog,
|
|
|
|
| 33 |
clearTraceLogs,
|
| 34 |
setPanelContent,
|
| 35 |
setPlan,
|
| 36 |
traceLogs,
|
|
|
|
|
|
|
| 37 |
} = useAgentStore();
|
| 38 |
|
| 39 |
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
|
@@ -55,34 +59,61 @@ export function useAgentWebSocket({
|
|
| 55 |
case 'processing':
|
| 56 |
setProcessing(true);
|
| 57 |
clearTraceLogs();
|
|
|
|
| 58 |
break;
|
| 59 |
|
| 60 |
case 'assistant_message': {
|
| 61 |
const content = (event.data?.content as string) || '';
|
| 62 |
const currentTrace = useAgentStore.getState().traceLogs;
|
| 63 |
-
const
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
content
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
break;
|
| 72 |
}
|
| 73 |
|
| 74 |
case 'tool_call': {
|
| 75 |
const toolName = (event.data?.tool as string) || 'unknown';
|
| 76 |
const args = (event.data?.arguments as Record<string, any>) || {};
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
// Auto-expand Right Panel for specific tools
|
| 87 |
if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
|
| 88 |
setPanelContent({
|
|
@@ -111,18 +142,21 @@ export function useAgentWebSocket({
|
|
| 111 |
const toolName = (event.data?.tool as string) || 'unknown';
|
| 112 |
const output = (event.data?.output as string) || '';
|
| 113 |
const success = event.data?.success as boolean;
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if (toolName === 'hf_jobs') {
|
| 116 |
-
// Find the last message with approval (likely the one that triggered this job)
|
| 117 |
const messages = useAgentStore.getState().getMessages(sessionId);
|
| 118 |
-
// Reverse to find the most recent one
|
| 119 |
const lastApprovalMsg = [...messages].reverse().find(m => m.approval);
|
| 120 |
-
|
| 121 |
if (lastApprovalMsg) {
|
| 122 |
-
// Append output if there's already some (for multiple jobs in batch)
|
| 123 |
const currentOutput = lastApprovalMsg.toolOutput || '';
|
| 124 |
const newOutput = currentOutput ? currentOutput + '\n\n' + output : output;
|
| 125 |
-
|
| 126 |
useAgentStore.getState().updateMessage(sessionId, lastApprovalMsg.id, {
|
| 127 |
toolOutput: newOutput
|
| 128 |
});
|
|
@@ -130,19 +164,9 @@ export function useAgentWebSocket({
|
|
| 130 |
} else {
|
| 131 |
console.warn('Received hf_jobs output but no approval message found to update.');
|
| 132 |
}
|
| 133 |
-
// CRITICAL: Always break for hf_jobs to prevent a separate "Tool" bubble from appearing
|
| 134 |
-
break;
|
| 135 |
}
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
id: `msg_tool_${Date.now()}`,
|
| 139 |
-
role: 'tool',
|
| 140 |
-
content: output,
|
| 141 |
-
timestamp: new Date().toISOString(),
|
| 142 |
-
toolName: toolName,
|
| 143 |
-
};
|
| 144 |
-
addMessage(sessionId, message);
|
| 145 |
-
|
| 146 |
console.log('Tool output:', toolName, success);
|
| 147 |
break;
|
| 148 |
}
|
|
@@ -218,6 +242,7 @@ export function useAgentWebSocket({
|
|
| 218 |
|
| 219 |
case 'turn_complete':
|
| 220 |
setProcessing(false);
|
|
|
|
| 221 |
break;
|
| 222 |
|
| 223 |
case 'compacted': {
|
|
|
|
| 25 |
|
| 26 |
const {
|
| 27 |
addMessage,
|
| 28 |
+
updateMessage,
|
| 29 |
setProcessing,
|
| 30 |
setConnected,
|
| 31 |
setPendingApprovals,
|
| 32 |
setError,
|
| 33 |
addTraceLog,
|
| 34 |
+
updateTraceLog,
|
| 35 |
clearTraceLogs,
|
| 36 |
setPanelContent,
|
| 37 |
setPlan,
|
| 38 |
traceLogs,
|
| 39 |
+
setCurrentTurnMessageId,
|
| 40 |
+
updateCurrentTurnTrace,
|
| 41 |
} = useAgentStore();
|
| 42 |
|
| 43 |
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
|
|
|
| 59 |
case 'processing':
|
| 60 |
setProcessing(true);
|
| 61 |
clearTraceLogs();
|
| 62 |
+
setCurrentTurnMessageId(null); // Start a new turn
|
| 63 |
break;
|
| 64 |
|
| 65 |
case 'assistant_message': {
|
| 66 |
const content = (event.data?.content as string) || '';
|
| 67 |
const currentTrace = useAgentStore.getState().traceLogs;
|
| 68 |
+
const currentTurnMsgId = useAgentStore.getState().currentTurnMessageId;
|
| 69 |
+
|
| 70 |
+
if (currentTurnMsgId) {
|
| 71 |
+
// Update existing message - append content and update trace
|
| 72 |
+
const messages = useAgentStore.getState().getMessages(sessionId);
|
| 73 |
+
const existingMsg = messages.find(m => m.id === currentTurnMsgId);
|
| 74 |
+
|
| 75 |
+
if (existingMsg) {
|
| 76 |
+
const newContent = existingMsg.content ? existingMsg.content + '\n\n' + content : content;
|
| 77 |
+
updateMessage(sessionId, currentTurnMsgId, {
|
| 78 |
+
content: newContent,
|
| 79 |
+
trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
// Create new message
|
| 84 |
+
const messageId = `msg_${Date.now()}`;
|
| 85 |
+
const message: Message = {
|
| 86 |
+
id: messageId,
|
| 87 |
+
role: 'assistant',
|
| 88 |
+
content,
|
| 89 |
+
timestamp: new Date().toISOString(),
|
| 90 |
+
trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
|
| 91 |
+
};
|
| 92 |
+
addMessage(sessionId, message);
|
| 93 |
+
setCurrentTurnMessageId(messageId);
|
| 94 |
+
}
|
| 95 |
break;
|
| 96 |
}
|
| 97 |
|
| 98 |
case 'tool_call': {
|
| 99 |
const toolName = (event.data?.tool as string) || 'unknown';
|
| 100 |
const args = (event.data?.arguments as Record<string, any>) || {};
|
| 101 |
+
|
| 102 |
+
// Don't display plan_tool in trace logs (it shows up elsewhere in the UI)
|
| 103 |
+
if (toolName !== 'plan_tool') {
|
| 104 |
+
const log: TraceLog = {
|
| 105 |
+
id: `tool_${Date.now()}`,
|
| 106 |
+
type: 'call',
|
| 107 |
+
text: `Agent is executing ${toolName}...`,
|
| 108 |
+
tool: toolName,
|
| 109 |
+
timestamp: new Date().toISOString(),
|
| 110 |
+
completed: false,
|
| 111 |
+
};
|
| 112 |
+
addTraceLog(log);
|
| 113 |
+
// Update the current turn message's trace in real-time
|
| 114 |
+
updateCurrentTurnTrace(sessionId);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
// Auto-expand Right Panel for specific tools
|
| 118 |
if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
|
| 119 |
setPanelContent({
|
|
|
|
| 142 |
const toolName = (event.data?.tool as string) || 'unknown';
|
| 143 |
const output = (event.data?.output as string) || '';
|
| 144 |
const success = event.data?.success as boolean;
|
| 145 |
+
|
| 146 |
+
// Mark the corresponding trace log as completed
|
| 147 |
+
updateTraceLog(toolName, { completed: true });
|
| 148 |
+
// Update the current turn message's trace in real-time
|
| 149 |
+
updateCurrentTurnTrace(sessionId);
|
| 150 |
+
|
| 151 |
+
// Special handling for hf_jobs - update the approval message with output
|
| 152 |
if (toolName === 'hf_jobs') {
|
|
|
|
| 153 |
const messages = useAgentStore.getState().getMessages(sessionId);
|
|
|
|
| 154 |
const lastApprovalMsg = [...messages].reverse().find(m => m.approval);
|
| 155 |
+
|
| 156 |
if (lastApprovalMsg) {
|
|
|
|
| 157 |
const currentOutput = lastApprovalMsg.toolOutput || '';
|
| 158 |
const newOutput = currentOutput ? currentOutput + '\n\n' + output : output;
|
| 159 |
+
|
| 160 |
useAgentStore.getState().updateMessage(sessionId, lastApprovalMsg.id, {
|
| 161 |
toolOutput: newOutput
|
| 162 |
});
|
|
|
|
| 164 |
} else {
|
| 165 |
console.warn('Received hf_jobs output but no approval message found to update.');
|
| 166 |
}
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
+
|
| 169 |
+
// Don't create message bubbles for tool outputs - they only show in trace logs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
console.log('Tool output:', toolName, success);
|
| 171 |
break;
|
| 172 |
}
|
|
|
|
| 242 |
|
| 243 |
case 'turn_complete':
|
| 244 |
setProcessing(false);
|
| 245 |
+
setCurrentTurnMessageId(null); // Clear the current turn
|
| 246 |
break;
|
| 247 |
|
| 248 |
case 'compacted': {
|
frontend/src/store/agentStore.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface AgentStore {
|
|
| 18 |
traceLogs: TraceLog[];
|
| 19 |
panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
|
| 20 |
plan: PlanItem[];
|
|
|
|
| 21 |
|
| 22 |
// Actions
|
| 23 |
addMessage: (sessionId: string, message: Message) => void;
|
|
@@ -30,9 +31,12 @@ interface AgentStore {
|
|
| 30 |
setError: (error: string | null) => void;
|
| 31 |
getMessages: (sessionId: string) => Message[];
|
| 32 |
addTraceLog: (log: TraceLog) => void;
|
|
|
|
| 33 |
clearTraceLogs: () => void;
|
| 34 |
setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
|
| 35 |
setPlan: (plan: PlanItem[]) => void;
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
export const useAgentStore = create<AgentStore>((set, get) => ({
|
|
@@ -45,6 +49,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|
| 45 |
traceLogs: [],
|
| 46 |
panelContent: null,
|
| 47 |
plan: [],
|
|
|
|
| 48 |
|
| 49 |
addMessage: (sessionId: string, message: Message) => {
|
| 50 |
set((state) => {
|
|
@@ -112,6 +117,20 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|
| 112 |
}));
|
| 113 |
},
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
clearTraceLogs: () => {
|
| 116 |
set({ traceLogs: [] });
|
| 117 |
},
|
|
@@ -123,4 +142,26 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|
| 123 |
setPlan: (plan: PlanItem[]) => {
|
| 124 |
set({ plan });
|
| 125 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
}));
|
|
|
|
| 18 |
traceLogs: TraceLog[];
|
| 19 |
panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
|
| 20 |
plan: PlanItem[];
|
| 21 |
+
currentTurnMessageId: string | null; // Track the current turn's assistant message
|
| 22 |
|
| 23 |
// Actions
|
| 24 |
addMessage: (sessionId: string, message: Message) => void;
|
|
|
|
| 31 |
setError: (error: string | null) => void;
|
| 32 |
getMessages: (sessionId: string) => Message[];
|
| 33 |
addTraceLog: (log: TraceLog) => void;
|
| 34 |
+
updateTraceLog: (toolName: string, updates: Partial<TraceLog>) => void;
|
| 35 |
clearTraceLogs: () => void;
|
| 36 |
setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
|
| 37 |
setPlan: (plan: PlanItem[]) => void;
|
| 38 |
+
setCurrentTurnMessageId: (id: string | null) => void;
|
| 39 |
+
updateCurrentTurnTrace: (sessionId: string) => void;
|
| 40 |
}
|
| 41 |
|
| 42 |
export const useAgentStore = create<AgentStore>((set, get) => ({
|
|
|
|
| 49 |
traceLogs: [],
|
| 50 |
panelContent: null,
|
| 51 |
plan: [],
|
| 52 |
+
currentTurnMessageId: null,
|
| 53 |
|
| 54 |
addMessage: (sessionId: string, message: Message) => {
|
| 55 |
set((state) => {
|
|
|
|
| 117 |
}));
|
| 118 |
},
|
| 119 |
|
| 120 |
+
updateTraceLog: (toolName: string, updates: Partial<TraceLog>) => {
|
| 121 |
+
set((state) => {
|
| 122 |
+
// Find the last trace log with this tool name and update it
|
| 123 |
+
const traceLogs = [...state.traceLogs];
|
| 124 |
+
for (let i = traceLogs.length - 1; i >= 0; i--) {
|
| 125 |
+
if (traceLogs[i].tool === toolName && traceLogs[i].type === 'call') {
|
| 126 |
+
traceLogs[i] = { ...traceLogs[i], ...updates };
|
| 127 |
+
break;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
return { traceLogs };
|
| 131 |
+
});
|
| 132 |
+
},
|
| 133 |
+
|
| 134 |
clearTraceLogs: () => {
|
| 135 |
set({ traceLogs: [] });
|
| 136 |
},
|
|
|
|
| 142 |
setPlan: (plan: PlanItem[]) => {
|
| 143 |
set({ plan });
|
| 144 |
},
|
| 145 |
+
|
| 146 |
+
setCurrentTurnMessageId: (id: string | null) => {
|
| 147 |
+
set({ currentTurnMessageId: id });
|
| 148 |
+
},
|
| 149 |
+
|
| 150 |
+
updateCurrentTurnTrace: (sessionId: string) => {
|
| 151 |
+
const state = get();
|
| 152 |
+
if (state.currentTurnMessageId) {
|
| 153 |
+
const currentMessages = state.messagesBySession[sessionId] || [];
|
| 154 |
+
const updatedMessages = currentMessages.map((msg) =>
|
| 155 |
+
msg.id === state.currentTurnMessageId
|
| 156 |
+
? { ...msg, trace: state.traceLogs.length > 0 ? [...state.traceLogs] : undefined }
|
| 157 |
+
: msg
|
| 158 |
+
);
|
| 159 |
+
set({
|
| 160 |
+
messagesBySession: {
|
| 161 |
+
...state.messagesBySession,
|
| 162 |
+
[sessionId]: updatedMessages,
|
| 163 |
+
},
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
}));
|
frontend/src/types/agent.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface TraceLog {
|
|
| 54 |
text: string;
|
| 55 |
tool: string;
|
| 56 |
timestamp: string;
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
export interface User {
|
|
|
|
| 54 |
text: string;
|
| 55 |
tool: string;
|
| 56 |
timestamp: string;
|
| 57 |
+
completed?: boolean;
|
| 58 |
}
|
| 59 |
|
| 60 |
export interface User {
|