akseljoonas HF Staff Claude Opus 4.5 commited on
Commit
b29799c
·
1 Parent(s): fc14c05

Improve job approval UX with tabs, job URL, and message ordering

Browse files

- Add tabbed panel for switching between Script and Logs views
- Show specific job URL from tool output (not generic jobs page link)
- Fix message ordering so final assistant response appears below approval box
- Persist logs in approval messages so users can access previous job logs
- Add View Script/View Logs buttons on completed approval cards
- Show job status and failure indicators in approval flow
- Panel tabs only clear on new job, not during approval continuation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

frontend/src/components/Chat/ApprovalFlow.tsx CHANGED
@@ -1,9 +1,10 @@
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';
@@ -14,7 +15,7 @@ interface ApprovalFlowProps {
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);
@@ -27,19 +28,45 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
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
 
@@ -125,11 +152,13 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
125
  const getToolDescription = (toolName: string, args: any) => {
126
  if (toolName === 'hf_jobs') {
127
  return (
128
- <Typography variant="body2" sx={{ color: 'var(--muted-text)', flex: 1 }}>
129
- The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>hf_jobs</Box> on{' '}
130
- <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.hardware_flavor || 'default'}</Box> with a timeout of{' '}
131
- <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.timeout || '30m'}</Box>
132
- </Typography>
 
 
133
  );
134
  }
135
  return (
@@ -142,33 +171,60 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
142
  const showCode = () => {
143
  const args = currentTool.arguments as any;
144
  if (currentTool.tool === 'hf_jobs' && args.script) {
145
- setPanelContent({
146
- title: 'Compute Job Script',
147
- content: args.script,
148
- language: 'python',
149
- parameters: args
 
 
 
 
 
 
 
 
 
 
 
150
  });
151
- setRightPanelOpen(true);
152
- setLeftSidebarOpen(false);
 
 
153
  } else {
154
- setPanelContent({
155
- title: `Tool: ${currentTool.tool}`,
156
- content: JSON.stringify(args, null, 2),
157
- language: 'json',
158
- parameters: args
159
- });
160
- setRightPanelOpen(true);
161
- setLeftSidebarOpen(false);
162
  }
163
  };
164
 
165
  const handleViewLogs = (e: React.MouseEvent) => {
166
  e.stopPropagation();
167
- setPanelContent({
168
- title: 'Job Logs',
169
- content: logsContent,
170
- language: 'text'
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  });
 
172
  setRightPanelOpen(true);
173
  setLeftSidebarOpen(false);
174
  };
@@ -221,31 +277,124 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
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"
233
- size="small"
234
- startIcon={<OpenInNewIcon />}
235
- onClick={handleViewLogs}
236
- sx={{
237
- alignSelf: 'flex-start',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  textTransform: 'none',
239
  borderColor: 'rgba(255,255,255,0.1)',
240
  color: 'var(--accent-primary)',
 
 
241
  '&:hover': {
242
- borderColor: 'var(--accent-primary)',
243
- bgcolor: 'rgba(255,255,255,0.03)'
244
  }
245
- }}
246
- >
247
- View Logs
248
- </Button>
 
 
249
  )}
250
 
251
  {status === 'pending' && (
 
1
  import { useState, useCallback, useEffect } from 'react';
2
+ import { Box, Typography, Button, TextField, IconButton, Link } 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 LaunchIcon from '@mui/icons-material/Launch';
8
  import { useAgentStore } from '@/store/agentStore';
9
  import { useLayoutStore } from '@/store/layoutStore';
10
  import { useSessionStore } from '@/store/sessionStore';
 
15
  }
16
 
17
  export default function ApprovalFlow({ message }: ApprovalFlowProps) {
18
+ const { setPanelContent, setPanelTab, setActivePanelTab, clearPanelTabs, updateMessage } = useAgentStore();
19
  const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
20
  const { activeSessionId } = useSessionStore();
21
  const [currentIndex, setCurrentIndex] = useState(0);
 
28
 
29
  const { batch, status } = approvalData;
30
 
31
+ // Parse toolOutput to extract job info (URL, status, logs)
32
  let logsContent = '';
33
  let showLogsButton = false;
34
+ let jobUrl = '';
35
+ let jobId = '';
36
+ let jobStatus = '';
37
+ let jobFailed = false;
38
+
39
+ if (message.toolOutput) {
40
+ // Extract job URL: **View at:** https://...
41
+ const urlMatch = message.toolOutput.match(/\*\*View at:\*\*\s*(https:\/\/[^\s\n]+)/);
42
+ if (urlMatch) {
43
+ jobUrl = urlMatch[1];
44
+ }
45
+
46
+ // Extract job ID: **Job ID:** ...
47
+ const idMatch = message.toolOutput.match(/\*\*Job ID:\*\*\s*([^\s\n]+)/);
48
+ if (idMatch) {
49
+ jobId = idMatch[1];
50
+ }
51
+
52
+ // Extract job status: **Final Status:** ...
53
+ const statusMatch = message.toolOutput.match(/\*\*Final Status:\*\*\s*([^\n]+)/);
54
+ if (statusMatch) {
55
+ jobStatus = statusMatch[1].trim();
56
+ jobFailed = jobStatus.toLowerCase().includes('error') || jobStatus.toLowerCase().includes('failed');
57
+ }
58
+
59
+ // Extract logs
60
+ if (message.toolOutput.includes('**Logs:**')) {
61
+ const parts = message.toolOutput.split('**Logs:**');
62
+ if (parts.length > 1) {
63
  const logsPart = parts[1].trim();
64
  const codeBlockMatch = logsPart.match(/```([\s\S]*?)```/);
65
  if (codeBlockMatch) {
66
+ logsContent = codeBlockMatch[1].trim();
67
+ showLogsButton = true;
68
  }
69
+ }
70
  }
71
  }
72
 
 
152
  const getToolDescription = (toolName: string, args: any) => {
153
  if (toolName === 'hf_jobs') {
154
  return (
155
+ <Box sx={{ flex: 1 }}>
156
+ <Typography variant="body2" sx={{ color: 'var(--muted-text)' }}>
157
+ The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>hf_jobs</Box> on{' '}
158
+ <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.hardware_flavor || 'default'}</Box> with a timeout of{' '}
159
+ <Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.timeout || '30m'}</Box>
160
+ </Typography>
161
+ </Box>
162
  );
163
  }
164
  return (
 
171
  const showCode = () => {
172
  const args = currentTool.arguments as any;
173
  if (currentTool.tool === 'hf_jobs' && args.script) {
174
+ // Clear existing tabs and set up script tab (and logs if available)
175
+ clearPanelTabs();
176
+ setPanelTab({
177
+ id: 'script',
178
+ title: 'Script',
179
+ content: args.script,
180
+ language: 'python',
181
+ parameters: args
182
+ });
183
+ // If logs are available (job completed), also add logs tab
184
+ if (logsContent) {
185
+ setPanelTab({
186
+ id: 'logs',
187
+ title: 'Logs',
188
+ content: logsContent,
189
+ language: 'text'
190
  });
191
+ }
192
+ setActivePanelTab('script');
193
+ setRightPanelOpen(true);
194
+ setLeftSidebarOpen(false);
195
  } else {
196
+ setPanelContent({
197
+ title: `Tool: ${currentTool.tool}`,
198
+ content: JSON.stringify(args, null, 2),
199
+ language: 'json',
200
+ parameters: args
201
+ });
202
+ setRightPanelOpen(true);
203
+ setLeftSidebarOpen(false);
204
  }
205
  };
206
 
207
  const handleViewLogs = (e: React.MouseEvent) => {
208
  e.stopPropagation();
209
+ const args = currentTool.arguments as any;
210
+ // Set up both tabs so user can switch between script and logs
211
+ clearPanelTabs();
212
+ if (currentTool.tool === 'hf_jobs' && args.script) {
213
+ setPanelTab({
214
+ id: 'script',
215
+ title: 'Script',
216
+ content: args.script,
217
+ language: 'python',
218
+ parameters: args
219
+ });
220
+ }
221
+ setPanelTab({
222
+ id: 'logs',
223
+ title: 'Logs',
224
+ content: logsContent,
225
+ language: 'text'
226
  });
227
+ setActivePanelTab('logs');
228
  setRightPanelOpen(true);
229
  setLeftSidebarOpen(false);
230
  };
 
277
  <OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
278
  </Box>
279
 
280
+ {currentTool.tool === 'hf_jobs' && (
281
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
282
+ {/* Show specific job URL if available (after execution), otherwise generic link */}
283
+ {jobUrl ? (
284
+ <Link
285
+ href={jobUrl}
286
+ target="_blank"
287
+ rel="noopener noreferrer"
288
+ sx={{
289
+ display: 'flex',
290
+ alignItems: 'center',
291
+ gap: 0.5,
292
+ color: 'var(--accent-primary)',
293
+ fontSize: '0.8rem',
294
+ textDecoration: 'none',
295
+ opacity: 0.9,
296
+ '&:hover': {
297
+ opacity: 1,
298
+ textDecoration: 'underline',
299
+ }
300
+ }}
301
+ >
302
+ <LaunchIcon sx={{ fontSize: 14 }} />
303
+ View job{jobId ? ` (${jobId.substring(0, 8)}...)` : ''} on Hugging Face
304
+ </Link>
305
+ ) : (
306
+ <Link
307
+ href="https://huggingface.co/settings/jobs"
308
+ target="_blank"
309
+ rel="noopener noreferrer"
310
+ sx={{
311
+ display: 'flex',
312
+ alignItems: 'center',
313
+ gap: 0.5,
314
+ color: 'var(--muted-text)',
315
+ fontSize: '0.8rem',
316
+ textDecoration: 'none',
317
+ opacity: 0.7,
318
+ '&:hover': {
319
+ opacity: 1,
320
+ textDecoration: 'underline',
321
+ }
322
+ }}
323
+ >
324
+ <LaunchIcon sx={{ fontSize: 14 }} />
325
+ View all jobs on Hugging Face
326
+ </Link>
327
+ )}
328
+
329
+ {/* Show job status if available */}
330
+ {jobStatus && (
331
+ <Typography
332
+ variant="caption"
333
+ sx={{
334
+ color: jobFailed ? 'var(--accent-red)' : 'var(--accent-green)',
335
+ fontSize: '0.75rem',
336
+ fontWeight: 500,
337
+ }}
338
+ >
339
+ Status: {jobStatus}
340
+ </Typography>
341
+ )}
342
+ </Box>
343
+ )}
344
+
345
  {containsPushToHub && (
346
  <Typography variant="caption" sx={{ color: 'var(--accent-green)', fontSize: '0.75rem', opacity: 0.8, px: 0.5 }}>
347
  We've detected the result will be pushed to hub.
348
  </Typography>
349
  )}
350
 
351
+ {/* Show script/logs buttons for completed jobs */}
352
+ {status !== 'pending' && currentTool.tool === 'hf_jobs' && (args.script || showLogsButton) && (
353
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
354
+ {args.script && (
355
+ <Button
356
+ variant="outlined"
357
+ size="small"
358
+ startIcon={<OpenInNewIcon />}
359
+ onClick={showCode}
360
+ sx={{
361
+ textTransform: 'none',
362
+ borderColor: 'rgba(255,255,255,0.1)',
363
+ color: 'var(--muted-text)',
364
+ fontSize: '0.75rem',
365
+ py: 0.5,
366
+ '&:hover': {
367
+ borderColor: 'var(--accent-primary)',
368
+ color: 'var(--accent-primary)',
369
+ bgcolor: 'rgba(255,255,255,0.03)'
370
+ }
371
+ }}
372
+ >
373
+ View Script
374
+ </Button>
375
+ )}
376
+ {showLogsButton && (
377
+ <Button
378
+ variant="outlined"
379
+ size="small"
380
+ startIcon={<OpenInNewIcon />}
381
+ onClick={handleViewLogs}
382
+ sx={{
383
  textTransform: 'none',
384
  borderColor: 'rgba(255,255,255,0.1)',
385
  color: 'var(--accent-primary)',
386
+ fontSize: '0.75rem',
387
+ py: 0.5,
388
  '&:hover': {
389
+ borderColor: 'var(--accent-primary)',
390
+ bgcolor: 'rgba(255,255,255,0.03)'
391
  }
392
+ }}
393
+ >
394
+ View Logs
395
+ </Button>
396
+ )}
397
+ </Box>
398
  )}
399
 
400
  {status === 'pending' && (
frontend/src/components/CodePanel/CodePanel.tsx CHANGED
@@ -1,9 +1,11 @@
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';
6
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
 
 
7
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
8
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
  import { useAgentStore } from '@/store/agentStore';
@@ -11,39 +13,83 @@ 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)' }}>
35
  {/* Header - Fixed 60px to align */}
36
- <Box sx={{
37
- height: '60px',
38
- display: 'flex',
39
- alignItems: 'center',
40
- justifyContent: 'space-between',
41
  px: 2,
42
  borderBottom: '1px solid rgba(255,255,255,0.03)'
43
  }}>
44
- <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
45
- {panelContent?.title || 'Code Panel'}
46
- </Typography>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  <IconButton size="small" onClick={() => setRightPanelOpen(false)} sx={{ color: 'var(--muted-text)' }}>
48
  <CloseIcon fontSize="small" />
49
  </IconButton>
@@ -51,66 +97,66 @@ export default function CodePanel() {
51
 
52
  {/* Main Content Area */}
53
  <Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
54
- {!panelContent ? (
55
- <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
56
  <Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}>
57
- NO DATA LOADED
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',
67
- borderRadius: 'var(--radius-md)',
68
- padding: '18px',
69
- border: '1px solid rgba(255,255,255,0.03)',
70
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
71
- fontSize: '13px',
72
- lineHeight: 1.55,
73
- height: '100%',
74
- overflow: 'auto',
75
- }}
76
  >
77
- {panelContent.content ? (
78
- panelContent.language === 'python' ? (
79
- <SyntaxHighlighter
80
- language="python"
81
- style={vscDarkPlus}
82
- customStyle={{
83
- margin: 0,
84
- padding: 0,
85
- background: 'transparent',
86
- fontSize: '13px',
87
- fontFamily: 'inherit',
88
- }}
89
- wrapLines={true}
90
- wrapLongLines={true}
91
- >
92
- {displayContent}
93
- </SyntaxHighlighter>
94
- ) : (
95
- <Box component="pre" sx={{
96
- m: 0,
97
- fontFamily: 'inherit',
98
- color: 'var(--text)',
99
- whiteSpace: 'pre-wrap',
100
- wordBreak: 'break-all'
101
- }}>
102
- <code>{displayContent}</code>
103
- </Box>
104
- )
105
  ) : (
106
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
107
- <Typography variant="caption">
108
- NO CONTENT TO DISPLAY
109
- </Typography>
110
- </Box>
111
- )}
112
- </Box>
 
 
 
 
 
 
 
 
 
 
113
  </Box>
 
114
  )}
115
  </Box>
116
 
 
1
  import { useRef, useEffect, useMemo } from 'react';
2
+ import { Box, Typography, IconButton, Tabs, Tab } 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';
6
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
7
+ import CodeIcon from '@mui/icons-material/Code';
8
+ import TerminalIcon from '@mui/icons-material/Terminal';
9
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
10
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
11
  import { useAgentStore } from '@/store/agentStore';
 
13
  import { processLogs } from '@/utils/logProcessor';
14
 
15
  export default function CodePanel() {
16
+ const { panelContent, panelTabs, activePanelTab, setActivePanelTab, plan } = useAgentStore();
17
  const { setRightPanelOpen } = useLayoutStore();
18
  const scrollRef = useRef<HTMLDivElement>(null);
19
 
20
+ // Get the active tab content, or fall back to panelContent for backwards compatibility
21
+ const activeTab = panelTabs.find(t => t.id === activePanelTab);
22
+ const currentContent = activeTab || panelContent;
23
+
24
  const displayContent = useMemo(() => {
25
+ if (!currentContent?.content) return '';
26
  // Apply log processing only for text/logs, not for code/json
27
+ if (!currentContent.language || currentContent.language === 'text') {
28
+ return processLogs(currentContent.content);
29
  }
30
+ return currentContent.content;
31
+ }, [currentContent?.content, currentContent?.language]);
32
 
33
  useEffect(() => {
34
+ // Auto-scroll only for logs tab
35
+ if (scrollRef.current && activePanelTab === 'logs') {
36
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
37
  }
38
+ }, [displayContent, activePanelTab]);
39
+
40
+ const hasTabs = panelTabs.length > 0;
41
 
42
  return (
43
  <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}>
44
  {/* Header - Fixed 60px to align */}
45
+ <Box sx={{
46
+ height: '60px',
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ justifyContent: 'space-between',
50
  px: 2,
51
  borderBottom: '1px solid rgba(255,255,255,0.03)'
52
  }}>
53
+ {hasTabs ? (
54
+ <Tabs
55
+ value={activePanelTab || panelTabs[0]?.id}
56
+ onChange={(_, newValue) => setActivePanelTab(newValue)}
57
+ sx={{
58
+ minHeight: 36,
59
+ '& .MuiTabs-indicator': {
60
+ backgroundColor: 'var(--accent-primary)',
61
+ },
62
+ '& .MuiTab-root': {
63
+ minHeight: 36,
64
+ minWidth: 'auto',
65
+ px: 2,
66
+ py: 0.5,
67
+ fontSize: '0.75rem',
68
+ fontWeight: 600,
69
+ textTransform: 'uppercase',
70
+ letterSpacing: '0.05em',
71
+ color: 'var(--muted-text)',
72
+ '&.Mui-selected': {
73
+ color: 'var(--text)',
74
+ },
75
+ },
76
+ }}
77
+ >
78
+ {panelTabs.map((tab) => (
79
+ <Tab
80
+ key={tab.id}
81
+ value={tab.id}
82
+ label={tab.title}
83
+ icon={tab.id === 'script' ? <CodeIcon sx={{ fontSize: 16 }} /> : <TerminalIcon sx={{ fontSize: 16 }} />}
84
+ iconPosition="start"
85
+ />
86
+ ))}
87
+ </Tabs>
88
+ ) : (
89
+ <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
90
+ {currentContent?.title || 'Code Panel'}
91
+ </Typography>
92
+ )}
93
  <IconButton size="small" onClick={() => setRightPanelOpen(false)} sx={{ color: 'var(--muted-text)' }}>
94
  <CloseIcon fontSize="small" />
95
  </IconButton>
 
97
 
98
  {/* Main Content Area */}
99
  <Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
100
+ {!currentContent ? (
101
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
102
  <Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}>
103
+ NO DATA LOADED
104
  </Typography>
105
+ </Box>
106
  ) : (
107
+ <Box sx={{ flex: 1, overflow: 'hidden', p: 2 }}>
108
+ <Box
109
+ ref={scrollRef}
110
+ className="code-panel"
111
+ sx={{
112
+ background: '#0A0B0C',
113
+ borderRadius: 'var(--radius-md)',
114
+ padding: '18px',
115
+ border: '1px solid rgba(255,255,255,0.03)',
116
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
117
+ fontSize: '13px',
118
+ lineHeight: 1.55,
119
+ height: '100%',
120
+ overflow: 'auto',
121
+ }}
122
  >
123
+ {currentContent.content ? (
124
+ currentContent.language === 'python' ? (
125
+ <SyntaxHighlighter
126
+ language="python"
127
+ style={vscDarkPlus}
128
+ customStyle={{
129
+ margin: 0,
130
+ padding: 0,
131
+ background: 'transparent',
132
+ fontSize: '13px',
133
+ fontFamily: 'inherit',
134
+ }}
135
+ wrapLines={true}
136
+ wrapLongLines={true}
137
+ >
138
+ {displayContent}
139
+ </SyntaxHighlighter>
 
 
 
 
 
 
 
 
 
 
 
140
  ) : (
141
+ <Box component="pre" sx={{
142
+ m: 0,
143
+ fontFamily: 'inherit',
144
+ color: 'var(--text)',
145
+ whiteSpace: 'pre-wrap',
146
+ wordBreak: 'break-all'
147
+ }}>
148
+ <code>{displayContent}</code>
149
+ </Box>
150
+ )
151
+ ) : (
152
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
153
+ <Typography variant="caption">
154
+ NO CONTENT TO DISPLAY
155
+ </Typography>
156
+ </Box>
157
+ )}
158
  </Box>
159
+ </Box>
160
  )}
161
  </Box>
162
 
frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -34,6 +34,9 @@ export function useAgentWebSocket({
34
  updateTraceLog,
35
  clearTraceLogs,
36
  setPanelContent,
 
 
 
37
  setPlan,
38
  setCurrentTurnMessageId,
39
  updateCurrentTurnTrace,
@@ -58,6 +61,8 @@ export function useAgentWebSocket({
58
  case 'processing':
59
  setProcessing(true);
60
  clearTraceLogs();
 
 
61
  setCurrentTurnMessageId(null); // Start a new turn
62
  break;
63
 
@@ -115,23 +120,29 @@ export function useAgentWebSocket({
115
 
116
  // Auto-expand Right Panel for specific tools
117
  if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
118
- setPanelContent({
119
- title: 'Compute Job Script',
 
 
 
 
120
  content: args.script,
121
- language: 'python'
 
122
  });
 
123
  setRightPanelOpen(true);
124
  setLeftSidebarOpen(false);
125
- } else if (toolName === 'hf_repo_files' && args.operation === 'upload' && args.content) {
126
- setPanelContent({
127
- title: `File Upload: ${args.path || 'unnamed'} `,
128
- content: args.content,
129
- parameters: args,
130
- language: args.path?.endsWith('.py') ? 'python' : undefined
131
- });
132
- setRightPanelOpen(true);
133
- setLeftSidebarOpen(false);
134
- }
135
 
136
  console.log('Tool call:', toolName, args);
137
  break;
@@ -175,29 +186,26 @@ export function useAgentWebSocket({
175
  const log = (event.data?.log as string) || '';
176
 
177
  if (toolName === 'hf_jobs') {
178
- const currentPanel = useAgentStore.getState().panelContent;
179
-
180
- // If we are already showing logs, append
181
- // If we are showing "Compute Job Script", overwrite/switch to logs
182
- // Otherwise, initialize
183
-
184
- let newContent = log;
185
- if (currentPanel?.title === 'Job Logs') {
186
- newContent = currentPanel.content + '\n' + log;
187
- } else if (currentPanel?.title === 'Compute Job Script') {
188
- // We were showing the script, now logs start.
189
- // Maybe we want to clear and start showing logs.
190
- newContent = '--- Starting execution ---\n' + log;
191
- }
192
 
193
- setPanelContent({
194
- title: 'Job Logs',
 
 
 
 
 
 
195
  content: newContent,
196
  language: 'text'
197
  });
198
-
 
 
 
199
  if (!useLayoutStore.getState().isRightPanelOpen) {
200
- setRightPanelOpen(true);
201
  }
202
  }
203
  break;
@@ -219,7 +227,7 @@ export function useAgentWebSocket({
219
  tool_call_id: string;
220
  }>;
221
  const count = (event.data?.count as number) || 0;
222
-
223
  // Create a persistent message for the approval request
224
  const message: Message = {
225
  id: `msg_approval_${Date.now()}`,
@@ -232,7 +240,10 @@ export function useAgentWebSocket({
232
  }
233
  };
234
  addMessage(sessionId, message);
235
-
 
 
 
236
  // We don't set pendingApprovals in the global store anymore as the message handles the UI
237
  setPendingApprovals(null);
238
  setProcessing(false);
 
34
  updateTraceLog,
35
  clearTraceLogs,
36
  setPanelContent,
37
+ setPanelTab,
38
+ setActivePanelTab,
39
+ clearPanelTabs,
40
  setPlan,
41
  setCurrentTurnMessageId,
42
  updateCurrentTurnTrace,
 
61
  case 'processing':
62
  setProcessing(true);
63
  clearTraceLogs();
64
+ // Don't clear panel tabs here - they should persist during approval flow
65
+ // Tabs will be cleared when a new tool_call sets up new content
66
  setCurrentTurnMessageId(null); // Start a new turn
67
  break;
68
 
 
120
 
121
  // Auto-expand Right Panel for specific tools
122
  if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
123
+ // Clear any existing tabs from previous jobs before setting new script
124
+ clearPanelTabs();
125
+ // Use tab system for jobs - add script tab immediately
126
+ setPanelTab({
127
+ id: 'script',
128
+ title: 'Script',
129
  content: args.script,
130
+ language: 'python',
131
+ parameters: args
132
  });
133
+ setActivePanelTab('script');
134
  setRightPanelOpen(true);
135
  setLeftSidebarOpen(false);
136
+ } else if (toolName === 'hf_repo_files' && args.operation === 'upload' && args.content) {
137
+ setPanelContent({
138
+ title: `File Upload: ${args.path || 'unnamed'}`,
139
+ content: args.content,
140
+ parameters: args,
141
+ language: args.path?.endsWith('.py') ? 'python' : undefined
142
+ });
143
+ setRightPanelOpen(true);
144
+ setLeftSidebarOpen(false);
145
+ }
146
 
147
  console.log('Tool call:', toolName, args);
148
  break;
 
186
  const log = (event.data?.log as string) || '';
187
 
188
  if (toolName === 'hf_jobs') {
189
+ const currentTabs = useAgentStore.getState().panelTabs;
190
+ const logsTab = currentTabs.find(t => t.id === 'logs');
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ // Append to existing logs tab or create new one
193
+ const newContent = logsTab
194
+ ? logsTab.content + '\n' + log
195
+ : '--- Job execution started ---\n' + log;
196
+
197
+ setPanelTab({
198
+ id: 'logs',
199
+ title: 'Logs',
200
  content: newContent,
201
  language: 'text'
202
  });
203
+
204
+ // Auto-switch to logs tab when logs start streaming
205
+ setActivePanelTab('logs');
206
+
207
  if (!useLayoutStore.getState().isRightPanelOpen) {
208
+ setRightPanelOpen(true);
209
  }
210
  }
211
  break;
 
227
  tool_call_id: string;
228
  }>;
229
  const count = (event.data?.count as number) || 0;
230
+
231
  // Create a persistent message for the approval request
232
  const message: Message = {
233
  id: `msg_approval_${Date.now()}`,
 
240
  }
241
  };
242
  addMessage(sessionId, message);
243
+
244
+ // Clear currentTurnMessageId so subsequent assistant_message events create a new message below the approval
245
+ setCurrentTurnMessageId(null);
246
+
247
  // We don't set pendingApprovals in the global store anymore as the message handles the UI
248
  setPendingApprovals(null);
249
  setProcessing(false);
frontend/src/store/agentStore.ts CHANGED
@@ -7,6 +7,14 @@ export interface PlanItem {
7
  status: 'pending' | 'in_progress' | 'completed';
8
  }
9
 
 
 
 
 
 
 
 
 
10
  interface AgentStore {
11
  // State per session (keyed by session ID)
12
  messagesBySession: Record<string, Message[]>;
@@ -17,6 +25,8 @@ interface AgentStore {
17
  error: string | null;
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
 
@@ -34,6 +44,9 @@ interface AgentStore {
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;
@@ -48,6 +61,8 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
48
  error: null,
49
  traceLogs: [],
50
  panelContent: null,
 
 
51
  plan: [],
52
  currentTurnMessageId: null,
53
 
@@ -139,6 +154,33 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
139
  set({ panelContent: content });
140
  },
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  setPlan: (plan: PlanItem[]) => {
143
  set({ plan });
144
  },
 
7
  status: 'pending' | 'in_progress' | 'completed';
8
  }
9
 
10
+ interface PanelTab {
11
+ id: string;
12
+ title: string;
13
+ content: string;
14
+ language?: string;
15
+ parameters?: any;
16
+ }
17
+
18
  interface AgentStore {
19
  // State per session (keyed by session ID)
20
  messagesBySession: Record<string, Message[]>;
 
25
  error: string | null;
26
  traceLogs: TraceLog[];
27
  panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
28
+ panelTabs: PanelTab[];
29
+ activePanelTab: string | null;
30
  plan: PlanItem[];
31
  currentTurnMessageId: string | null; // Track the current turn's assistant message
32
 
 
44
  updateTraceLog: (toolName: string, updates: Partial<TraceLog>) => void;
45
  clearTraceLogs: () => void;
46
  setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
47
+ setPanelTab: (tab: PanelTab) => void;
48
+ setActivePanelTab: (tabId: string) => void;
49
+ clearPanelTabs: () => void;
50
  setPlan: (plan: PlanItem[]) => void;
51
  setCurrentTurnMessageId: (id: string | null) => void;
52
  updateCurrentTurnTrace: (sessionId: string) => void;
 
61
  error: null,
62
  traceLogs: [],
63
  panelContent: null,
64
+ panelTabs: [],
65
+ activePanelTab: null,
66
  plan: [],
67
  currentTurnMessageId: null,
68
 
 
154
  set({ panelContent: content });
155
  },
156
 
157
+ setPanelTab: (tab: PanelTab) => {
158
+ set((state) => {
159
+ const existingIndex = state.panelTabs.findIndex(t => t.id === tab.id);
160
+ let newTabs: PanelTab[];
161
+ if (existingIndex >= 0) {
162
+ // Update existing tab
163
+ newTabs = [...state.panelTabs];
164
+ newTabs[existingIndex] = tab;
165
+ } else {
166
+ // Add new tab
167
+ newTabs = [...state.panelTabs, tab];
168
+ }
169
+ return {
170
+ panelTabs: newTabs,
171
+ activePanelTab: state.activePanelTab || tab.id, // Auto-select first tab
172
+ };
173
+ });
174
+ },
175
+
176
+ setActivePanelTab: (tabId: string) => {
177
+ set({ activePanelTab: tabId });
178
+ },
179
+
180
+ clearPanelTabs: () => {
181
+ set({ panelTabs: [], activePanelTab: null });
182
+ },
183
+
184
  setPlan: (plan: PlanItem[]) => {
185
  set({ plan });
186
  },