Henri Bonamy commited on
Commit
35dc01a
Β·
1 Parent(s): 2523e77

logs have a button to see, improved rejection/approved flow, trying to handle tqdm

Browse files
frontend/src/components/Chat/ApprovalFlow.tsx CHANGED
@@ -1,31 +1,56 @@
1
  import { useState, useCallback, useEffect } from 'react';
2
- import { Box, Typography, Button, TextField, Divider } from '@mui/material';
 
 
 
 
3
  import { useAgentStore } from '@/store/agentStore';
4
  import { useLayoutStore } from '@/store/layoutStore';
 
 
5
 
6
  interface ApprovalFlowProps {
7
- sessionId: string;
8
  }
9
 
10
- export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
11
- const { pendingApprovals, setPendingApprovals, setPanelContent } = useAgentStore();
12
  const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
 
13
  const [currentIndex, setCurrentIndex] = useState(0);
14
  const [feedback, setFeedback] = useState('');
15
  const [decisions, setDecisions] = useState<Array<{ tool_call_id: string; approved: boolean; feedback: string | null }>>([]);
16
 
17
- // Reset local state when a new batch of approvals arrives
18
- useEffect(() => {
19
- setCurrentIndex(0);
20
- setFeedback('');
21
- setDecisions([]);
22
- }, [pendingApprovals]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  // Sync right panel with current tool
25
  useEffect(() => {
26
- if (!pendingApprovals || currentIndex >= pendingApprovals.tools.length) return;
 
 
 
27
 
28
- const tool = pendingApprovals.tools[currentIndex];
29
  const args = tool.arguments as any;
30
 
31
  if (tool.tool === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
@@ -35,30 +60,20 @@ export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
35
  language: 'python',
36
  parameters: args
37
  });
38
- setRightPanelOpen(true);
39
- setLeftSidebarOpen(false);
40
  } else if (tool.tool === 'hf_repo_files' && args.operation === 'upload' && args.content) {
41
  setPanelContent({
42
  title: `File Upload: ${args.path || 'unnamed'}`,
43
  content: args.content,
44
  parameters: args
45
  });
46
- setRightPanelOpen(true);
47
- setLeftSidebarOpen(false);
48
- } else {
49
- // For other tools, just show parameters in the panel
50
- setPanelContent({
51
- title: `Tool: ${tool.tool}`,
52
- content: '',
53
- parameters: args
54
- });
55
  }
56
- }, [currentIndex, pendingApprovals, setPanelContent, setRightPanelOpen, setLeftSidebarOpen]);
57
 
58
  const handleResolve = useCallback(async (approved: boolean) => {
59
- if (!pendingApprovals) return;
60
 
61
- const currentTool = pendingApprovals.tools[currentIndex];
62
  const newDecisions = [
63
  ...decisions,
64
  {
@@ -68,40 +83,97 @@ export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
68
  },
69
  ];
70
 
71
- if (currentIndex < pendingApprovals.tools.length - 1) {
72
  setDecisions(newDecisions);
73
  setCurrentIndex(currentIndex + 1);
74
  setFeedback('');
75
  } else {
76
- // All tools in batch resolved, submit to backend
77
  try {
78
  await fetch('/api/approve', {
79
  method: 'POST',
80
  headers: { 'Content-Type': 'application/json' },
81
  body: JSON.stringify({
82
- session_id: sessionId,
83
  approvals: newDecisions,
84
  }),
85
  });
86
- setPendingApprovals(null);
 
 
 
 
 
 
 
 
 
87
  } catch (e) {
88
  console.error('Approval submission failed:', e);
89
  }
90
  }
91
- }, [sessionId, pendingApprovals, currentIndex, feedback, decisions, setPendingApprovals]);
92
 
93
- if (!pendingApprovals || currentIndex >= pendingApprovals.tools.length) return null;
94
 
95
- const currentTool = pendingApprovals.tools[currentIndex];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  return (
98
  <Box
99
  className="action-card"
100
  sx={{
101
- mt: 2,
102
- mb: 4,
103
  width: '100%',
104
- alignSelf: 'center',
105
  padding: '18px',
106
  borderRadius: 'var(--radius-md)',
107
  background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
@@ -109,51 +181,104 @@ export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
109
  display: 'flex',
110
  flexDirection: 'column',
111
  gap: '12px',
 
112
  }}
113
  >
114
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
115
  <Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'var(--text)' }}>
116
- Approval Required
117
  </Typography>
118
  <Typography variant="caption" sx={{ color: 'var(--muted-text)' }}>
119
- ({currentIndex + 1}/{pendingApprovals.count})
120
  </Typography>
 
 
121
  </Box>
122
 
123
- <Typography variant="body2" sx={{ color: 'var(--muted-text)' }}>
124
- The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>{currentTool.tool}</Box>
125
- </Typography>
126
-
127
- <Box component="pre" sx={{
128
- bgcolor: 'rgba(0,0,0,0.3)',
129
- p: 2,
130
- borderRadius: '8px',
131
- fontSize: '0.8rem',
132
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
133
- overflow: 'auto',
134
- maxHeight: 200,
135
- border: '1px solid rgba(255,255,255,0.05)',
136
- margin: 0
137
- }}>
138
- {JSON.stringify(currentTool.arguments, null, 2)}
 
 
 
 
139
  </Box>
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
142
- <TextField
143
- fullWidth
144
- size="small"
145
- placeholder="Feedback (optional)"
146
- value={feedback}
147
- onChange={(e) => setFeedback(e.target.value)}
148
- variant="outlined"
149
- sx={{
150
- '& .MuiOutlinedInput-root': {
151
- bgcolor: 'rgba(0,0,0,0.2)',
152
- fontFamily: 'inherit',
153
- fontSize: '0.9rem'
154
- }
155
- }}
156
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  <Box className="action-buttons" sx={{ display: 'flex', gap: '10px' }}>
159
  <Button
@@ -194,6 +319,13 @@ export default function ApprovalFlow({ sessionId }: ApprovalFlowProps) {
194
  </Button>
195
  </Box>
196
  </Box>
 
 
 
 
 
 
 
197
  </Box>
198
  );
199
- }
 
1
  import { useState, useCallback, useEffect } from 'react';
2
+ import { Box, Typography, Button, TextField, IconButton } from '@mui/material';
3
+ import SendIcon from '@mui/icons-material/Send';
4
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
5
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
6
+ import CancelIcon from '@mui/icons-material/Cancel';
7
  import { useAgentStore } from '@/store/agentStore';
8
  import { useLayoutStore } from '@/store/layoutStore';
9
+ import { useSessionStore } from '@/store/sessionStore';
10
+ import type { Message } from '@/types/agent';
11
 
12
  interface ApprovalFlowProps {
13
+ message: Message;
14
  }
15
 
16
+ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
17
+ const { setPanelContent, updateMessage } = useAgentStore();
18
  const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
19
+ const { activeSessionId } = useSessionStore();
20
  const [currentIndex, setCurrentIndex] = useState(0);
21
  const [feedback, setFeedback] = useState('');
22
  const [decisions, setDecisions] = useState<Array<{ tool_call_id: string; approved: boolean; feedback: string | null }>>([]);
23
 
24
+ const approvalData = message.approval;
25
+
26
+ if (!approvalData) return null;
27
+
28
+ const { batch, status } = approvalData;
29
+
30
+ // Extract logs from toolOutput if available
31
+ let logsContent = '';
32
+ let showLogsButton = false;
33
+
34
+ if (message.toolOutput && message.toolOutput.includes('**Logs:**')) {
35
+ const parts = message.toolOutput.split('**Logs:**');
36
+ if (parts.length > 1) {
37
+ const logsPart = parts[1].trim();
38
+ const codeBlockMatch = logsPart.match(/```([\s\S]*?)```/);
39
+ if (codeBlockMatch) {
40
+ logsContent = codeBlockMatch[1].trim();
41
+ showLogsButton = true;
42
+ }
43
+ }
44
+ }
45
 
46
  // Sync right panel with current tool
47
  useEffect(() => {
48
+ if (!batch || currentIndex >= batch.tools.length) return;
49
+
50
+ // Only auto-open panel if pending
51
+ if (status !== 'pending') return;
52
 
53
+ const tool = batch.tools[currentIndex];
54
  const args = tool.arguments as any;
55
 
56
  if (tool.tool === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
 
60
  language: 'python',
61
  parameters: args
62
  });
63
+ // Don't auto-open if already resolved
 
64
  } else if (tool.tool === 'hf_repo_files' && args.operation === 'upload' && args.content) {
65
  setPanelContent({
66
  title: `File Upload: ${args.path || 'unnamed'}`,
67
  content: args.content,
68
  parameters: args
69
  });
 
 
 
 
 
 
 
 
 
70
  }
71
+ }, [currentIndex, batch, status, setPanelContent]);
72
 
73
  const handleResolve = useCallback(async (approved: boolean) => {
74
+ if (!batch || !activeSessionId) return;
75
 
76
+ const currentTool = batch.tools[currentIndex];
77
  const newDecisions = [
78
  ...decisions,
79
  {
 
83
  },
84
  ];
85
 
86
+ if (currentIndex < batch.tools.length - 1) {
87
  setDecisions(newDecisions);
88
  setCurrentIndex(currentIndex + 1);
89
  setFeedback('');
90
  } else {
91
+ // All tools in batch resolved
92
  try {
93
  await fetch('/api/approve', {
94
  method: 'POST',
95
  headers: { 'Content-Type': 'application/json' },
96
  body: JSON.stringify({
97
+ session_id: activeSessionId,
98
  approvals: newDecisions,
99
  }),
100
  });
101
+
102
+ // Update message status
103
+ updateMessage(activeSessionId, message.id, {
104
+ approval: {
105
+ ...approvalData!,
106
+ status: approved ? 'approved' : 'rejected',
107
+ decisions: newDecisions
108
+ }
109
+ });
110
+
111
  } catch (e) {
112
  console.error('Approval submission failed:', e);
113
  }
114
  }
115
+ }, [activeSessionId, message.id, batch, currentIndex, feedback, decisions, approvalData, updateMessage]);
116
 
117
+ if (!batch || currentIndex >= batch.tools.length) return null;
118
 
119
+ const currentTool = batch.tools[currentIndex];
120
+
121
+ const getToolDescription = (toolName: string, args: any) => {
122
+ if (toolName === 'hf_jobs') {
123
+ return (
124
+ <Typography variant="body2" sx={{ color: 'var(--muted-text)', flex: 1 }}>
125
+ The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>hf_jobs</Box> on{' '}
126
+ <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.hardware_flavor || 'default'}</Box> with a timeout of{' '}
127
+ <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.timeout || '30m'}</Box>
128
+ </Typography>
129
+ );
130
+ }
131
+ return (
132
+ <Typography variant="body2" sx={{ color: 'var(--muted-text)', flex: 1 }}>
133
+ The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>{toolName}</Box>
134
+ </Typography>
135
+ );
136
+ };
137
+
138
+ const showCode = () => {
139
+ const args = currentTool.arguments as any;
140
+ if (currentTool.tool === 'hf_jobs' && args.script) {
141
+ setPanelContent({
142
+ title: 'Compute Job Script',
143
+ content: args.script,
144
+ language: 'python',
145
+ parameters: args
146
+ });
147
+ setRightPanelOpen(true);
148
+ setLeftSidebarOpen(false);
149
+ } else {
150
+ setPanelContent({
151
+ title: `Tool: ${currentTool.tool}`,
152
+ content: JSON.stringify(args, null, 2),
153
+ language: 'json',
154
+ parameters: args
155
+ });
156
+ setRightPanelOpen(true);
157
+ setLeftSidebarOpen(false);
158
+ }
159
+ };
160
+
161
+ const handleViewLogs = (e: React.MouseEvent) => {
162
+ e.stopPropagation();
163
+ setPanelContent({
164
+ title: 'Job Logs',
165
+ content: logsContent,
166
+ language: 'text'
167
+ });
168
+ setRightPanelOpen(true);
169
+ setLeftSidebarOpen(false);
170
+ };
171
 
172
  return (
173
  <Box
174
  className="action-card"
175
  sx={{
 
 
176
  width: '100%',
 
177
  padding: '18px',
178
  borderRadius: 'var(--radius-md)',
179
  background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
 
181
  display: 'flex',
182
  flexDirection: 'column',
183
  gap: '12px',
184
+ opacity: status !== 'pending' && !showLogsButton ? 0.8 : 1
185
  }}
186
  >
187
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
188
  <Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'var(--text)' }}>
189
+ {status === 'pending' ? 'Approval Required' : status === 'approved' ? 'Approved' : 'Rejected'}
190
  </Typography>
191
  <Typography variant="caption" sx={{ color: 'var(--muted-text)' }}>
192
+ ({currentIndex + 1}/{batch.count})
193
  </Typography>
194
+ {status === 'approved' && <CheckCircleIcon sx={{ fontSize: 18, color: 'var(--accent-green)' }} />}
195
+ {status === 'rejected' && <CancelIcon sx={{ fontSize: 18, color: 'var(--accent-red)' }} />}
196
  </Box>
197
 
198
+ <Box
199
+ onClick={showCode}
200
+ sx={{
201
+ display: 'flex',
202
+ alignItems: 'center',
203
+ gap: 1,
204
+ cursor: 'pointer',
205
+ p: 1.5,
206
+ borderRadius: '8px',
207
+ bgcolor: 'rgba(0,0,0,0.2)',
208
+ border: '1px solid rgba(255,255,255,0.05)',
209
+ transition: 'all 0.2s',
210
+ '&:hover': {
211
+ bgcolor: 'rgba(255,255,255,0.03)',
212
+ borderColor: 'var(--accent-primary)',
213
+ }
214
+ }}
215
+ >
216
+ {getToolDescription(currentTool.tool, currentTool.arguments)}
217
+ <OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
218
  </Box>
219
 
220
+ {showLogsButton && (
221
+ <Button
222
+ variant="outlined"
223
+ size="small"
224
+ startIcon={<OpenInNewIcon />}
225
+ onClick={handleViewLogs}
226
+ sx={{
227
+ alignSelf: 'flex-start',
228
+ textTransform: 'none',
229
+ borderColor: 'rgba(255,255,255,0.1)',
230
+ color: 'var(--accent-primary)',
231
+ '&:hover': {
232
+ borderColor: 'var(--accent-primary)',
233
+ bgcolor: 'rgba(255,255,255,0.03)'
234
+ }
235
+ }}
236
+ >
237
+ View Logs
238
+ </Button>
239
+ )}
240
+
241
+ {status === 'pending' && (
242
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
243
+ <Box sx={{ display: 'flex', gap: 1 }}>
244
+ <TextField
245
+ fullWidth
246
+ size="small"
247
+ placeholder="Feedback (optional)"
248
+ value={feedback}
249
+ onChange={(e) => setFeedback(e.target.value)}
250
+ variant="outlined"
251
+ sx={{
252
+ '& .MuiOutlinedInput-root': {
253
+ bgcolor: 'rgba(0,0,0,0.2)',
254
+ fontFamily: 'inherit',
255
+ fontSize: '0.9rem'
256
+ }
257
+ }}
258
+ />
259
+ <IconButton
260
+ onClick={() => handleResolve(false)}
261
+ disabled={!feedback}
262
+ title="Reject with feedback"
263
+ sx={{
264
+ color: 'var(--accent-red)',
265
+ border: '1px solid rgba(255,255,255,0.05)',
266
+ borderRadius: '8px',
267
+ width: 40,
268
+ height: 40,
269
+ '&:hover': {
270
+ bgcolor: 'rgba(224, 90, 79, 0.1)',
271
+ borderColor: 'var(--accent-red)',
272
+ },
273
+ '&.Mui-disabled': {
274
+ color: 'rgba(255,255,255,0.1)',
275
+ borderColor: 'rgba(255,255,255,0.02)'
276
+ }
277
+ }}
278
+ >
279
+ <SendIcon fontSize="small" />
280
+ </IconButton>
281
+ </Box>
282
 
283
  <Box className="action-buttons" sx={{ display: 'flex', gap: '10px' }}>
284
  <Button
 
319
  </Button>
320
  </Box>
321
  </Box>
322
+ )}
323
+
324
+ {status === 'rejected' && decisions.some(d => d.feedback) && (
325
+ <Typography variant="body2" sx={{ color: 'var(--accent-red)', mt: 1 }}>
326
+ Feedback: {decisions.find(d => d.feedback)?.feedback}
327
+ </Typography>
328
+ )}
329
  </Box>
330
  );
331
+ }
frontend/src/components/Chat/MessageBubble.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { Box, Paper, Typography, Chip } from '@mui/material';
2
  import ReactMarkdown from 'react-markdown';
3
  import remarkGfm from 'remark-gfm';
 
4
  import type { Message } from '@/types/agent';
5
 
6
  interface MessageBubbleProps {
@@ -12,6 +13,14 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
12
  const isTool = message.role === 'tool';
13
  const isAssistant = message.role === 'assistant';
14
 
 
 
 
 
 
 
 
 
15
  return (
16
  <Box
17
  sx={{
 
1
  import { Box, Paper, Typography, Chip } from '@mui/material';
2
  import ReactMarkdown from 'react-markdown';
3
  import remarkGfm from 'remark-gfm';
4
+ import ApprovalFlow from './ApprovalFlow';
5
  import type { Message } from '@/types/agent';
6
 
7
  interface MessageBubbleProps {
 
13
  const isTool = message.role === 'tool';
14
  const isAssistant = message.role === 'assistant';
15
 
16
+ if (message.approval) {
17
+ return (
18
+ <Box sx={{ width: '100%', maxWidth: '880px', mx: 'auto', my: 2 }}>
19
+ <ApprovalFlow message={message} />
20
+ </Box>
21
+ );
22
+ }
23
+
24
  return (
25
  <Box
26
  sx={{
frontend/src/components/Chat/MessageList.tsx CHANGED
@@ -3,7 +3,6 @@ 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 ApprovalFlow from './ApprovalFlow';
7
  import type { Message } from '@/types/agent';
8
 
9
  interface MessageListProps {
@@ -131,7 +130,8 @@ export default function MessageList({ messages, isProcessing }: MessageListProps
131
  )}
132
 
133
  {activeSessionId && (
134
- <ApprovalFlow sessionId={activeSessionId} />
 
135
  )}
136
 
137
  <div ref={bottomRef} />
 
3
  import { useAgentStore } from '@/store/agentStore';
4
  import { useSessionStore } from '@/store/sessionStore';
5
  import MessageBubble from './MessageBubble';
 
6
  import type { Message } from '@/types/agent';
7
 
8
  interface MessageListProps {
 
130
  )}
131
 
132
  {activeSessionId && (
133
+ // ApprovalFlow is now handled within messages
134
+ null
135
  )}
136
 
137
  <div ref={bottomRef} />
frontend/src/components/CodePanel/CodePanel.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { Box, Typography, IconButton, Checkbox } from '@mui/material';
 
2
  import CloseIcon from '@mui/icons-material/Close';
3
  import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
4
  import CheckCircleIcon from '@mui/icons-material/CheckCircle';
@@ -7,10 +8,27 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
8
  import { useAgentStore } from '@/store/agentStore';
9
  import { useLayoutStore } from '@/store/layoutStore';
 
10
 
11
  export default function CodePanel() {
12
  const { panelContent, plan } = useAgentStore();
13
  const { setRightPanelOpen } = useLayoutStore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  return (
16
  <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}>
@@ -40,8 +58,9 @@ export default function CodePanel() {
40
  </Typography>
41
  </Box>
42
  ) : (
43
- <Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
44
  <Box
 
45
  className="code-panel"
46
  sx={{
47
  background: '#0A0B0C',
@@ -70,7 +89,7 @@ export default function CodePanel() {
70
  wrapLines={true}
71
  wrapLongLines={true}
72
  >
73
- {panelContent.content}
74
  </SyntaxHighlighter>
75
  ) : (
76
  <Box component="pre" sx={{
@@ -80,7 +99,7 @@ export default function CodePanel() {
80
  whiteSpace: 'pre-wrap',
81
  wordBreak: 'break-all'
82
  }}>
83
- <code>{panelContent.content}</code>
84
  </Box>
85
  )
86
  ) : (
 
1
+ import { useRef, useEffect, useMemo } from 'react';
2
+ import { Box, Typography, IconButton } from '@mui/material';
3
  import CloseIcon from '@mui/icons-material/Close';
4
  import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
5
  import CheckCircleIcon from '@mui/icons-material/CheckCircle';
 
8
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
  import { useAgentStore } from '@/store/agentStore';
10
  import { useLayoutStore } from '@/store/layoutStore';
11
+ import { processLogs } from '@/utils/logProcessor';
12
 
13
  export default function CodePanel() {
14
  const { panelContent, plan } = useAgentStore();
15
  const { setRightPanelOpen } = useLayoutStore();
16
+ const scrollRef = useRef<HTMLDivElement>(null);
17
+
18
+ const displayContent = useMemo(() => {
19
+ if (!panelContent?.content) return '';
20
+ // Apply log processing only for text/logs, not for code/json
21
+ if (!panelContent.language || panelContent.language === 'text') {
22
+ return processLogs(panelContent.content);
23
+ }
24
+ return panelContent.content;
25
+ }, [panelContent?.content, panelContent?.language]);
26
+
27
+ useEffect(() => {
28
+ if (scrollRef.current) {
29
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
30
+ }
31
+ }, [displayContent]);
32
 
33
  return (
34
  <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}>
 
58
  </Typography>
59
  </Box>
60
  ) : (
61
+ <Box sx={{ flex: 1, overflow: 'hidden', p: 2 }}>
62
  <Box
63
+ ref={scrollRef}
64
  className="code-panel"
65
  sx={{
66
  background: '#0A0B0C',
 
89
  wrapLines={true}
90
  wrapLongLines={true}
91
  >
92
+ {displayContent}
93
  </SyntaxHighlighter>
94
  ) : (
95
  <Box component="pre" sx={{
 
99
  whiteSpace: 'pre-wrap',
100
  wordBreak: 'break-all'
101
  }}>
102
+ <code>{displayContent}</code>
103
  </Box>
104
  )
105
  ) : (
frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -111,7 +111,38 @@ 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
- // Only log output to console, not to trace logs per user request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  console.log('Tool output:', toolName, success);
116
  break;
117
  }
@@ -165,7 +196,22 @@ export function useAgentWebSocket({
165
  tool_call_id: string;
166
  }>;
167
  const count = (event.data?.count as number) || 0;
168
- setPendingApprovals({ tools, count });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  setProcessing(false);
170
  break;
171
  }
 
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
+ });
129
+ console.log('Updated approval message with tool output:', toolName);
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
+ const message: Message = {
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
  }
 
196
  tool_call_id: string;
197
  }>;
198
  const count = (event.data?.count as number) || 0;
199
+
200
+ // Create a persistent message for the approval request
201
+ const message: Message = {
202
+ id: `msg_approval_${Date.now()}`,
203
+ role: 'assistant',
204
+ content: '', // Content is handled by the approval UI
205
+ timestamp: new Date().toISOString(),
206
+ approval: {
207
+ status: 'pending',
208
+ batch: { tools, count }
209
+ }
210
+ };
211
+ addMessage(sessionId, message);
212
+
213
+ // We don't set pendingApprovals in the global store anymore as the message handles the UI
214
+ setPendingApprovals(null);
215
  setProcessing(false);
216
  break;
217
  }
frontend/src/store/agentStore.ts CHANGED
@@ -21,6 +21,7 @@ interface AgentStore {
21
 
22
  // Actions
23
  addMessage: (sessionId: string, message: Message) => void;
 
24
  clearMessages: (sessionId: string) => void;
25
  setProcessing: (isProcessing: boolean) => void;
26
  setConnected: (isConnected: boolean) => void;
@@ -57,6 +58,21 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
57
  });
58
  },
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  clearMessages: (sessionId: string) => {
61
  set((state) => ({
62
  messagesBySession: {
 
21
 
22
  // Actions
23
  addMessage: (sessionId: string, message: Message) => void;
24
+ updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => void;
25
  clearMessages: (sessionId: string) => void;
26
  setProcessing: (isProcessing: boolean) => void;
27
  setConnected: (isConnected: boolean) => void;
 
58
  });
59
  },
60
 
61
+ updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => {
62
+ set((state) => {
63
+ const currentMessages = state.messagesBySession[sessionId] || [];
64
+ const updatedMessages = currentMessages.map((msg) =>
65
+ msg.id === messageId ? { ...msg, ...updates } : msg
66
+ );
67
+ return {
68
+ messagesBySession: {
69
+ ...state.messagesBySession,
70
+ [sessionId]: updatedMessages,
71
+ },
72
+ };
73
+ });
74
+ },
75
+
76
  clearMessages: (sessionId: string) => {
77
  set((state) => ({
78
  messagesBySession: {
frontend/src/types/agent.ts CHANGED
@@ -17,6 +17,12 @@ export interface Message {
17
  toolName?: string;
18
  toolCallId?: string;
19
  trace?: TraceLog[];
 
 
 
 
 
 
20
  }
21
 
22
  export interface ToolCall {
 
17
  toolName?: string;
18
  toolCallId?: string;
19
  trace?: TraceLog[];
20
+ approval?: {
21
+ status: 'pending' | 'approved' | 'rejected';
22
+ batch: ApprovalBatch;
23
+ decisions?: ToolApproval[];
24
+ };
25
+ toolOutput?: string;
26
  }
27
 
28
  export interface ToolCall {
frontend/src/utils/logProcessor.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function processLogs(logs: string): string {
2
+ if (!logs) return '';
3
+
4
+ // 1. Handle \r (Carriage Return) for progress bars
5
+ const rawLines = logs.split('\n');
6
+ const processedLines: string[] = [];
7
+
8
+ for (const rawLine of rawLines) {
9
+ // Remove potential trailing \r from \r\n split
10
+ let line = rawLine;
11
+ if (line.endsWith('\r')) {
12
+ line = line.slice(0, -1);
13
+ }
14
+
15
+ if (line.includes('\r')) {
16
+ const segments = line.split('\r');
17
+ // Find the last non-empty segment
18
+ // Iterate backwards
19
+ let found = false;
20
+ for (let i = segments.length - 1; i >= 0; i--) {
21
+ if (segments[i].length > 0) {
22
+ processedLines.push(segments[i]);
23
+ found = true;
24
+ break;
25
+ }
26
+ }
27
+ if (!found) {
28
+ // If all segments were empty, push empty string (or skip?)
29
+ processedLines.push("");
30
+ }
31
+ } else {
32
+ processedLines.push(line);
33
+ }
34
+ }
35
+
36
+ // 2. Compaction (Downloading & TQDM)
37
+ const finalLines: string[] = [];
38
+
39
+ // Regex for "Downloading <package>" or "Downloaded <package>"
40
+ const downloadPattern = /^(Downloading|Downloaded)\s+/;
41
+
42
+ // Regex for TQDM-like progress bars
43
+ // Examples:
44
+ // "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 10/10 [00:01<00:00, 8.00it/s]"
45
+ // " 20%|## | ..."
46
+ // "Downloading: 10%"
47
+ const tqdmPattern = /^\s*\d+%\|.*\||^\s*\d+%\s+/;
48
+
49
+ for (let i = 0; i < processedLines.length; i++) {
50
+ const line = processedLines[i];
51
+
52
+ // Check for Download pattern
53
+ if (downloadPattern.test(line)) {
54
+ // Look ahead for consecutive download lines
55
+ let nextIsDownload = false;
56
+ if (i + 1 < processedLines.length) {
57
+ nextIsDownload = downloadPattern.test(processedLines[i + 1]);
58
+ }
59
+
60
+ if (nextIsDownload) {
61
+ continue; // Skip this line
62
+ }
63
+ }
64
+ // Check for TQDM pattern
65
+ else if (tqdmPattern.test(line)) {
66
+ // Look ahead for consecutive TQDM lines
67
+ let nextIsTqdm = false;
68
+ if (i + 1 < processedLines.length) {
69
+ nextIsTqdm = tqdmPattern.test(processedLines[i + 1]);
70
+ }
71
+
72
+ if (nextIsTqdm) {
73
+ continue; // Skip this line
74
+ }
75
+ }
76
+
77
+ finalLines.push(line);
78
+ }
79
+
80
+ return finalLines.join('\n');
81
+ }