akseljoonas HF Staff commited on
Commit
bbfa431
·
1 Parent(s): da44165

frontend update

Browse files
agent/context_manager/manager.py CHANGED
@@ -2,6 +2,7 @@
2
  Context management for conversation history
3
  """
4
 
 
5
  import zoneinfo
6
  from datetime import datetime
7
  from pathlib import Path
@@ -53,6 +54,10 @@ class ContextManager:
53
  current_time = now.strftime("%H:%M:%S.%f")[:-3]
54
  current_timezone = f"{now.strftime('%Z')} (UTC{now.strftime('%z')[:3]}:{now.strftime('%z')[3:]})"
55
 
 
 
 
 
56
  template = Template(template_str)
57
  return template.render(
58
  tools=tool_specs,
@@ -60,7 +65,7 @@ class ContextManager:
60
  current_date=current_date,
61
  current_time=current_time,
62
  current_timezone=current_timezone,
63
- hf_user_info=HfApi().whoami().get("name"),
64
  )
65
 
66
  def add_message(self, message: Message, token_count: int = None) -> None:
 
2
  Context management for conversation history
3
  """
4
 
5
+ import os
6
  import zoneinfo
7
  from datetime import datetime
8
  from pathlib import Path
 
54
  current_time = now.strftime("%H:%M:%S.%f")[:-3]
55
  current_timezone = f"{now.strftime('%Z')} (UTC{now.strftime('%z')[:3]}:{now.strftime('%z')[3:]})"
56
 
57
+ # Get HF user info with explicit token from env
58
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
59
+ hf_user_info = HfApi(token=hf_token).whoami().get("name", "unknown")
60
+
61
  template = Template(template_str)
62
  return template.render(
63
  tools=tool_specs,
 
65
  current_date=current_date,
66
  current_time=current_time,
67
  current_timezone=current_timezone,
68
+ hf_user_info=hf_user_info,
69
  )
70
 
71
  def add_message(self, message: Message, token_count: int = None) -> None:
agent/prompts/system_prompt_v2.yaml CHANGED
@@ -490,6 +490,29 @@ system_prompt: |
490
  - One-word answers when appropriate for simple questions
491
  - For complex tasks, provide structured breakdown
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  # Examples
494
 
495
  <example>
 
490
  - One-word answers when appropriate for simple questions
491
  - For complex tasks, provide structured breakdown
492
 
493
+ # ⚠️ CRITICAL: Task Completion Requirements
494
+
495
+ **You must FULLY satisfy the user's request before finishing your turn.** Do not stop prematurely.
496
+
497
+ **Before ending your turn, verify:**
498
+ 1. ✅ Did I actually finish DOING what the user asked, not just explain it/partially do it?
499
+ 2. ✅ Did I confirm the task succeeded (job submitted, file uploaded, etc.)?
500
+ 3. ✅ If I encountered an error, did I fix it and retry?
501
+ 4. ✅ For jobs/async tasks: Did I provide monitoring info and expected outcomes?
502
+
503
+ **Common mistakes to avoid:**
504
+ - ✗ Stopping after "I'll help you with X" without actually doing X
505
+ - ✗ Explaining what you WOULD do instead of DOING it
506
+ - ✗ Ending after a tool call fails without retrying or fixing
507
+ - ✗ Stopping mid-task because you described what happens next
508
+ - ✗ Not providing final summary with URLs/results after completing
509
+
510
+ **Correct behavior:**
511
+ - ✓ Continue calling tools until the task is actually complete
512
+ - ✓ After submitting a job, provide the job URL and monitoring links
513
+ - ✓ After an error, diagnose and fix it, then retry
514
+ - ✓ End with a clear summary of what was accomplished and any next steps
515
+
516
  # Examples
517
 
518
  <example>
agent/tools/jobs_tool.py CHANGED
@@ -1015,8 +1015,13 @@ async def hf_jobs_handler(
1015
  Event(event_type="tool_log", data={"tool": "hf_jobs", "log": log})
1016
  )
1017
 
 
 
 
 
1018
  tool = HfJobsTool(
1019
- namespace=os.environ.get("HF_NAMESPACE", ""),
 
1020
  log_callback=log_callback if session else None,
1021
  )
1022
  result = await tool.execute(arguments)
 
1015
  Event(event_type="tool_log", data={"tool": "hf_jobs", "log": log})
1016
  )
1017
 
1018
+ # Get token and namespace from HF token
1019
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
1020
+ namespace = HfApi(token=hf_token).whoami().get("name") if hf_token else None
1021
+
1022
  tool = HfJobsTool(
1023
+ namespace=namespace,
1024
+ hf_token=hf_token,
1025
  log_callback=log_callback if session else None,
1026
  )
1027
  result = await tool.execute(arguments)
configs/main_agent_config.json CHANGED
@@ -3,8 +3,8 @@
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
- "confirm_cpu_jobs": true,
7
- "auto_file_upload": false,
8
  "mcpServers": {
9
  "hf-mcp-server": {
10
  "transport": "http",
 
3
  "save_sessions": true,
4
  "session_dataset_repo": "akseljoonas/hf-agent-sessions",
5
  "yolo_mode": false,
6
+ "confirm_cpu_jobs": false,
7
+ "auto_file_upload": true,
8
  "mcpServers": {
9
  "hf-mcp-server": {
10
  "transport": "http",
frontend/src/components/Chat/ApprovalFlow.tsx CHANGED
@@ -28,37 +28,33 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
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]*?)```/);
@@ -68,6 +64,20 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
68
  }
69
  }
70
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
 
73
  // Sync right panel with current tool
@@ -277,44 +287,68 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
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',
@@ -322,7 +356,7 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
322
  }}
323
  >
324
  <LaunchIcon sx={{ fontSize: 14 }} />
325
- View all jobs on Hugging Face
326
  </Link>
327
  )}
328
 
@@ -348,55 +382,46 @@ export default function ApprovalFlow({ message }: ApprovalFlowProps) {
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' && (
401
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
402
  <Box sx={{ display: 'flex', gap: 1 }}>
 
28
 
29
  const { batch, status } = approvalData;
30
 
31
+ // Parse toolOutput to extract job info (URL, status, logs, errors)
32
  let logsContent = '';
33
  let showLogsButton = false;
34
  let jobUrl = '';
 
35
  let jobStatus = '';
36
  let jobFailed = false;
37
+ let errorMessage = '';
38
 
39
  if (message.toolOutput) {
40
+ const output = message.toolOutput;
41
+
42
  // Extract job URL: **View at:** https://...
43
+ const urlMatch = output.match(/\*\*View at:\*\*\s*(https:\/\/[^\s\n]+)/);
44
  if (urlMatch) {
45
  jobUrl = urlMatch[1];
46
  }
47
 
 
 
 
 
 
 
48
  // Extract job status: **Final Status:** ...
49
+ const statusMatch = output.match(/\*\*Final Status:\*\*\s*([^\n]+)/);
50
  if (statusMatch) {
51
  jobStatus = statusMatch[1].trim();
52
  jobFailed = jobStatus.toLowerCase().includes('error') || jobStatus.toLowerCase().includes('failed');
53
  }
54
 
55
  // Extract logs
56
+ if (output.includes('**Logs:**')) {
57
+ const parts = output.split('**Logs:**');
58
  if (parts.length > 1) {
59
  const logsPart = parts[1].trim();
60
  const codeBlockMatch = logsPart.match(/```([\s\S]*?)```/);
 
64
  }
65
  }
66
  }
67
+
68
+ // Detect errors - if output exists but doesn't have the expected job completion format
69
+ // This catches early failures (validation errors, API errors, etc.)
70
+ const isExpectedFormat = output.includes('**Job ID:**') || output.includes('**View at:**');
71
+ const looksLikeError = output.toLowerCase().includes('error') ||
72
+ output.toLowerCase().includes('failed') ||
73
+ output.toLowerCase().includes('exception') ||
74
+ output.includes('Traceback');
75
+
76
+ if (!isExpectedFormat || (looksLikeError && !logsContent)) {
77
+ // This is likely an error message - show it
78
+ errorMessage = output;
79
+ jobFailed = true;
80
+ }
81
  }
82
 
83
  // Sync right panel with current tool
 
287
  <OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
288
  </Box>
289
 
290
+ {/* Script/Logs buttons for hf_jobs - always show when we have a script */}
291
+ {currentTool.tool === 'hf_jobs' && args.script && (
292
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
293
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
294
+ <Button
295
+ variant="outlined"
296
+ size="small"
297
+ onClick={showCode}
298
  sx={{
299
+ textTransform: 'none',
300
+ borderColor: 'rgba(255,255,255,0.1)',
301
+ color: 'var(--muted-text)',
302
+ fontSize: '0.75rem',
303
+ py: 0.5,
 
 
304
  '&:hover': {
305
+ borderColor: 'var(--accent-primary)',
306
+ color: 'var(--accent-primary)',
307
+ bgcolor: 'rgba(255,255,255,0.03)'
308
  }
309
  }}
310
  >
311
+ View Script
312
+ </Button>
313
+ <Button
314
+ variant="outlined"
315
+ size="small"
316
+ onClick={handleViewLogs}
317
+ disabled={!logsContent && status === 'pending'}
318
+ sx={{
319
+ textTransform: 'none',
320
+ borderColor: 'rgba(255,255,255,0.1)',
321
+ color: logsContent ? 'var(--accent-primary)' : 'var(--muted-text)',
322
+ fontSize: '0.75rem',
323
+ py: 0.5,
324
+ '&:hover': {
325
+ borderColor: 'var(--accent-primary)',
326
+ bgcolor: 'rgba(255,255,255,0.03)'
327
+ },
328
+ '&.Mui-disabled': {
329
+ color: 'rgba(255,255,255,0.3)',
330
+ borderColor: 'rgba(255,255,255,0.05)',
331
+ }
332
+ }}
333
+ >
334
+ {logsContent ? 'View Logs' : 'Logs (waiting for job...)'}
335
+ </Button>
336
+ </Box>
337
+
338
+ {/* Job URL - only show when we have a specific URL */}
339
+ {jobUrl && (
340
  <Link
341
+ href={jobUrl}
342
  target="_blank"
343
  rel="noopener noreferrer"
344
  sx={{
345
  display: 'flex',
346
  alignItems: 'center',
347
  gap: 0.5,
348
+ color: 'var(--accent-primary)',
349
+ fontSize: '0.75rem',
350
  textDecoration: 'none',
351
+ opacity: 0.9,
352
  '&:hover': {
353
  opacity: 1,
354
  textDecoration: 'underline',
 
356
  }}
357
  >
358
  <LaunchIcon sx={{ fontSize: 14 }} />
359
+ View Job on Hugging Face
360
  </Link>
361
  )}
362
 
 
382
  </Typography>
383
  )}
384
 
385
+ {/* Show error message if job failed */}
386
+ {errorMessage && status !== 'pending' && (
387
+ <Box
388
+ sx={{
389
+ p: 1.5,
390
+ borderRadius: '8px',
391
+ bgcolor: 'rgba(224, 90, 79, 0.1)',
392
+ border: '1px solid rgba(224, 90, 79, 0.3)',
393
+ }}
394
+ >
395
+ <Typography
396
+ variant="caption"
397
+ sx={{
398
+ color: 'var(--accent-red)',
399
+ fontWeight: 600,
400
+ display: 'block',
401
+ mb: 0.5,
402
+ }}
403
+ >
404
+ Error
405
+ </Typography>
406
+ <Typography
407
+ component="pre"
408
+ sx={{
409
+ color: 'var(--text)',
410
+ fontSize: '0.75rem',
411
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
412
+ whiteSpace: 'pre-wrap',
413
+ wordBreak: 'break-word',
414
+ m: 0,
415
+ maxHeight: '150px',
416
+ overflow: 'auto',
417
+ }}
418
+ >
419
+ {errorMessage.length > 500 ? errorMessage.substring(0, 500) + '...' : errorMessage}
420
+ </Typography>
 
 
 
 
 
 
 
 
 
 
421
  </Box>
422
  )}
423
 
424
+
425
  {status === 'pending' && (
426
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
427
  <Box sx={{ display: 'flex', gap: 1 }}>
frontend/src/components/Chat/MessageBubble.tsx CHANGED
@@ -1,26 +1,180 @@
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 {
8
  message: Message;
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  export default function MessageBubble({ message }: MessageBubbleProps) {
12
  const isUser = message.role === 'user';
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={{
@@ -37,7 +191,6 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
37
  sx={{
38
  p: '14px 18px',
39
  margin: '10px 0',
40
- width: isTool ? '100%' : 'auto',
41
  maxWidth: '100%',
42
  borderRadius: 'var(--radius-lg)',
43
  borderTopLeftRadius: isAssistant ? '6px' : undefined,
@@ -47,130 +200,14 @@ export default function MessageBubble({ message }: MessageBubbleProps) {
47
  background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
48
  }}
49
  >
50
- {isTool && (
51
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
52
- <Typography variant="caption" color="text.secondary">
53
- Tool
54
- </Typography>
55
- {message.toolName && (
56
- <Chip
57
- label={message.toolName}
58
- size="small"
59
- variant="outlined"
60
- sx={{ ml: 1, height: 20, fontSize: '0.7rem' }}
61
- />
62
- )}
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
69
- '& pre': {
70
- bgcolor: 'rgba(0,0,0,0.5)',
71
- p: 1.5,
72
- borderRadius: 1,
73
- overflow: 'auto',
74
- fontSize: '0.85rem',
75
- border: '1px solid rgba(255,255,255,0.05)',
76
- },
77
- '& code': {
78
- bgcolor: 'rgba(255,255,255,0.05)',
79
- px: 0.5,
80
- py: 0.25,
81
- borderRadius: 0.5,
82
- fontSize: '0.85rem',
83
- fontFamily: '"JetBrains Mono", monospace',
84
- },
85
- '& pre code': {
86
- bgcolor: 'transparent',
87
- p: 0,
88
- },
89
- '& a': {
90
- color: 'var(--accent-yellow)',
91
- textDecoration: 'none',
92
- '&:hover': {
93
- textDecoration: 'underline',
94
- },
95
- },
96
- '& ul, & ol': {
97
- pl: 2,
98
- my: 1,
99
- },
100
- '& table': {
101
- borderCollapse: 'collapse',
102
- width: '100%',
103
- my: 2,
104
- fontSize: '0.875rem',
105
- },
106
- '& th': {
107
- borderBottom: '1px solid',
108
- borderColor: 'rgba(255,255,255,0.1)',
109
- textAlign: 'left',
110
- p: 1,
111
- bgcolor: 'rgba(255,255,255,0.02)',
112
- },
113
- '& td': {
114
- borderBottom: '1px solid',
115
- borderColor: 'rgba(255,255,255,0.05)',
116
- p: 1,
117
- },
118
- }}
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>
175
  </Paper>
176
  </Box>
 
1
+ import { Box, Paper, Typography } from '@mui/material';
2
  import ReactMarkdown from 'react-markdown';
3
  import remarkGfm from 'remark-gfm';
4
  import ApprovalFlow from './ApprovalFlow';
5
+ import type { Message, TraceLog } from '@/types/agent';
6
+ import { useAgentStore } from '@/store/agentStore';
7
+ import { useLayoutStore } from '@/store/layoutStore';
8
 
9
  interface MessageBubbleProps {
10
  message: Message;
11
  }
12
 
13
+ // Render a tools segment with clickable tool calls
14
+ function ToolsSegment({ tools }: { tools: TraceLog[] }) {
15
+ const { showToolOutput } = useAgentStore();
16
+ const { setRightPanelOpen } = useLayoutStore();
17
+
18
+ const handleToolClick = (log: TraceLog) => {
19
+ if (log.completed && log.output) {
20
+ showToolOutput(log);
21
+ setRightPanelOpen(true);
22
+ }
23
+ };
24
+
25
+ return (
26
+ <Box
27
+ sx={{
28
+ bgcolor: 'rgba(0,0,0,0.3)',
29
+ borderRadius: 1,
30
+ p: 1.5,
31
+ border: 1,
32
+ borderColor: 'rgba(255,255,255,0.05)',
33
+ my: 1.5,
34
+ }}
35
+ >
36
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
37
+ {tools.map((log) => {
38
+ const isClickable = log.completed && log.output;
39
+ return (
40
+ <Typography
41
+ key={log.id}
42
+ variant="caption"
43
+ component="div"
44
+ onClick={() => handleToolClick(log)}
45
+ sx={{
46
+ color: 'var(--muted-text)',
47
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
48
+ fontSize: '0.75rem',
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ gap: 0.5,
52
+ cursor: isClickable ? 'pointer' : 'default',
53
+ borderRadius: 0.5,
54
+ px: 0.5,
55
+ mx: -0.5,
56
+ transition: 'background-color 0.15s ease',
57
+ '&:hover': isClickable ? {
58
+ bgcolor: 'rgba(255,255,255,0.05)',
59
+ } : {},
60
+ }}
61
+ >
62
+ <span style={{
63
+ color: log.completed
64
+ ? (log.success === false ? '#F87171' : '#FDB022')
65
+ : 'inherit',
66
+ fontSize: '0.85rem',
67
+ }}>
68
+ {log.completed ? (log.success === false ? '✗' : '✓') : '•'}
69
+ </span>
70
+ <span style={{
71
+ fontWeight: 600,
72
+ color: isClickable ? 'rgba(255, 255, 255, 0.9)' : 'inherit',
73
+ textDecoration: isClickable ? 'underline' : 'none',
74
+ textDecorationColor: 'rgba(255,255,255,0.3)',
75
+ textUnderlineOffset: '2px',
76
+ }}>
77
+ {log.tool}
78
+ </span>
79
+ {!log.completed && <span style={{ opacity: 0.6 }}>...</span>}
80
+ {isClickable && (
81
+ <span style={{
82
+ opacity: 0.4,
83
+ fontSize: '0.65rem',
84
+ marginLeft: 'auto',
85
+ }}>
86
+ click to view
87
+ </span>
88
+ )}
89
+ </Typography>
90
+ );
91
+ })}
92
+ </Box>
93
+ </Box>
94
+ );
95
+ }
96
+
97
+ // Markdown styles
98
+ const markdownStyles = {
99
+ '& p': { m: 0, mb: 1, '&:last-child': { mb: 0 } },
100
+ '& pre': {
101
+ bgcolor: 'rgba(0,0,0,0.5)',
102
+ p: 1.5,
103
+ borderRadius: 1,
104
+ overflow: 'auto',
105
+ fontSize: '0.85rem',
106
+ border: '1px solid rgba(255,255,255,0.05)',
107
+ },
108
+ '& code': {
109
+ bgcolor: 'rgba(255,255,255,0.05)',
110
+ px: 0.5,
111
+ py: 0.25,
112
+ borderRadius: 0.5,
113
+ fontSize: '0.85rem',
114
+ fontFamily: '"JetBrains Mono", monospace',
115
+ },
116
+ '& pre code': { bgcolor: 'transparent', p: 0 },
117
+ '& a': {
118
+ color: 'var(--accent-yellow)',
119
+ textDecoration: 'none',
120
+ '&:hover': { textDecoration: 'underline' },
121
+ },
122
+ '& ul, & ol': { pl: 2, my: 1 },
123
+ '& table': {
124
+ borderCollapse: 'collapse',
125
+ width: '100%',
126
+ my: 2,
127
+ fontSize: '0.875rem',
128
+ },
129
+ '& th': {
130
+ borderBottom: '1px solid rgba(255,255,255,0.1)',
131
+ textAlign: 'left',
132
+ p: 1,
133
+ bgcolor: 'rgba(255,255,255,0.02)',
134
+ },
135
+ '& td': {
136
+ borderBottom: '1px solid rgba(255,255,255,0.05)',
137
+ p: 1,
138
+ },
139
+ };
140
+
141
  export default function MessageBubble({ message }: MessageBubbleProps) {
142
  const isUser = message.role === 'user';
 
143
  const isAssistant = message.role === 'assistant';
144
 
145
  if (message.approval) {
146
  return (
147
+ <Box sx={{ width: '100%', maxWidth: '880px', mx: 'auto', my: 2 }}>
148
+ <ApprovalFlow message={message} />
149
+ </Box>
150
  );
151
  }
152
 
153
+ // Render segments chronologically if available, otherwise fall back to content
154
+ const renderContent = () => {
155
+ if (message.segments && message.segments.length > 0) {
156
+ return message.segments.map((segment, idx) => {
157
+ if (segment.type === 'text' && segment.content) {
158
+ return (
159
+ <Box key={idx} sx={markdownStyles}>
160
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{segment.content}</ReactMarkdown>
161
+ </Box>
162
+ );
163
+ }
164
+ if (segment.type === 'tools' && segment.tools && segment.tools.length > 0) {
165
+ return <ToolsSegment key={idx} tools={segment.tools} />;
166
+ }
167
+ return null;
168
+ });
169
+ }
170
+ // Fallback: just render content
171
+ return (
172
+ <Box sx={markdownStyles}>
173
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
174
+ </Box>
175
+ );
176
+ };
177
+
178
  return (
179
  <Box
180
  sx={{
 
191
  sx={{
192
  p: '14px 18px',
193
  margin: '10px 0',
 
194
  maxWidth: '100%',
195
  borderRadius: 'var(--radius-lg)',
196
  borderTopLeftRadius: isAssistant ? '6px' : undefined,
 
200
  background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
201
  }}
202
  >
203
+ {renderContent()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
+ <Typography
206
+ className="meta"
207
+ variant="caption"
208
+ sx={{ display: 'block', textAlign: 'right', mt: 1, fontSize: '11px', opacity: 0.5 }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  >
210
+ {new Date(message.timestamp).toLocaleTimeString()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </Typography>
212
  </Paper>
213
  </Box>
frontend/src/components/CodePanel/CodePanel.tsx CHANGED
@@ -1,19 +1,22 @@
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';
12
  import { useLayoutStore } from '@/store/layoutStore';
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
 
@@ -51,40 +54,72 @@ export default function CodePanel() {
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'}
@@ -137,6 +172,86 @@ export default function CodePanel() {
137
  >
138
  {displayContent}
139
  </SyntaxHighlighter>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  ) : (
141
  <Box component="pre" sx={{
142
  m: 0,
 
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 CodeIcon from '@mui/icons-material/Code';
8
  import TerminalIcon from '@mui/icons-material/Terminal';
9
+ import ArticleIcon from '@mui/icons-material/Article';
10
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
11
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
12
+ import ReactMarkdown from 'react-markdown';
13
+ import remarkGfm from 'remark-gfm';
14
  import { useAgentStore } from '@/store/agentStore';
15
  import { useLayoutStore } from '@/store/layoutStore';
16
  import { processLogs } from '@/utils/logProcessor';
17
 
18
  export default function CodePanel() {
19
+ const { panelContent, panelTabs, activePanelTab, setActivePanelTab, removePanelTab, plan } = useAgentStore();
20
  const { setRightPanelOpen } = useLayoutStore();
21
  const scrollRef = useRef<HTMLDivElement>(null);
22
 
 
54
  borderBottom: '1px solid rgba(255,255,255,0.03)'
55
  }}>
56
  {hasTabs ? (
57
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
58
+ {panelTabs.map((tab) => {
59
+ const isActive = activePanelTab === tab.id;
60
+ // Choose icon based on tab type
61
+ let icon = <TerminalIcon sx={{ fontSize: 14 }} />;
62
+ if (tab.id === 'script' || tab.language === 'python') {
63
+ icon = <CodeIcon sx={{ fontSize: 14 }} />;
64
+ } else if (tab.id === 'tool_output' || tab.language === 'markdown' || tab.language === 'json') {
65
+ icon = <ArticleIcon sx={{ fontSize: 14 }} />;
66
+ }
67
+ return (
68
+ <Box
69
+ key={tab.id}
70
+ onClick={() => setActivePanelTab(tab.id)}
71
+ sx={{
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ gap: 0.5,
75
+ px: 1.5,
76
+ py: 0.75,
77
+ borderRadius: 1,
78
+ cursor: 'pointer',
79
+ fontSize: '0.7rem',
80
+ fontWeight: 600,
81
+ textTransform: 'uppercase',
82
+ letterSpacing: '0.05em',
83
+ color: isActive ? 'var(--text)' : 'var(--muted-text)',
84
+ bgcolor: isActive ? 'rgba(255,255,255,0.08)' : 'transparent',
85
+ border: '1px solid',
86
+ borderColor: isActive ? 'rgba(255,255,255,0.1)' : 'transparent',
87
+ transition: 'all 0.15s ease',
88
+ '&:hover': {
89
+ bgcolor: 'rgba(255,255,255,0.05)',
90
+ },
91
+ }}
92
+ >
93
+ {icon}
94
+ <span>{tab.title}</span>
95
+ <Box
96
+ component="span"
97
+ onClick={(e) => {
98
+ e.stopPropagation();
99
+ removePanelTab(tab.id);
100
+ }}
101
+ sx={{
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ ml: 0.5,
106
+ width: 16,
107
+ height: 16,
108
+ borderRadius: '50%',
109
+ fontSize: '0.65rem',
110
+ opacity: 0.5,
111
+ '&:hover': {
112
+ opacity: 1,
113
+ bgcolor: 'rgba(255,255,255,0.1)',
114
+ },
115
+ }}
116
+ >
117
+
118
+ </Box>
119
+ </Box>
120
+ );
121
+ })}
122
+ </Box>
123
  ) : (
124
  <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
125
  {currentContent?.title || 'Code Panel'}
 
172
  >
173
  {displayContent}
174
  </SyntaxHighlighter>
175
+ ) : currentContent.language === 'json' ? (
176
+ <SyntaxHighlighter
177
+ language="json"
178
+ style={vscDarkPlus}
179
+ customStyle={{
180
+ margin: 0,
181
+ padding: 0,
182
+ background: 'transparent',
183
+ fontSize: '13px',
184
+ fontFamily: 'inherit',
185
+ }}
186
+ wrapLines={true}
187
+ wrapLongLines={true}
188
+ >
189
+ {displayContent}
190
+ </SyntaxHighlighter>
191
+ ) : currentContent.language === 'markdown' ? (
192
+ <Box sx={{
193
+ color: 'var(--text)',
194
+ fontSize: '13px',
195
+ lineHeight: 1.6,
196
+ '& p': { m: 0, mb: 1.5, '&:last-child': { mb: 0 } },
197
+ '& pre': {
198
+ bgcolor: 'rgba(0,0,0,0.4)',
199
+ p: 1.5,
200
+ borderRadius: 1,
201
+ overflow: 'auto',
202
+ fontSize: '12px',
203
+ border: '1px solid rgba(255,255,255,0.05)',
204
+ },
205
+ '& code': {
206
+ bgcolor: 'rgba(255,255,255,0.05)',
207
+ px: 0.5,
208
+ py: 0.25,
209
+ borderRadius: 0.5,
210
+ fontSize: '12px',
211
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
212
+ },
213
+ '& pre code': { bgcolor: 'transparent', p: 0 },
214
+ '& a': {
215
+ color: 'var(--accent-yellow)',
216
+ textDecoration: 'none',
217
+ '&:hover': { textDecoration: 'underline' },
218
+ },
219
+ '& ul, & ol': { pl: 2.5, my: 1 },
220
+ '& li': { mb: 0.5 },
221
+ '& table': {
222
+ borderCollapse: 'collapse',
223
+ width: '100%',
224
+ my: 2,
225
+ fontSize: '12px',
226
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
227
+ },
228
+ '& th': {
229
+ borderBottom: '2px solid rgba(255,255,255,0.15)',
230
+ textAlign: 'left',
231
+ p: 1,
232
+ fontWeight: 600,
233
+ },
234
+ '& td': {
235
+ borderBottom: '1px solid rgba(255,255,255,0.05)',
236
+ p: 1,
237
+ },
238
+ '& h1, & h2, & h3, & h4': {
239
+ mt: 2,
240
+ mb: 1,
241
+ fontWeight: 600,
242
+ },
243
+ '& h1': { fontSize: '1.25rem' },
244
+ '& h2': { fontSize: '1.1rem' },
245
+ '& h3': { fontSize: '1rem' },
246
+ '& blockquote': {
247
+ borderLeft: '3px solid rgba(255,255,255,0.2)',
248
+ pl: 2,
249
+ ml: 0,
250
+ color: 'var(--muted-text)',
251
+ },
252
+ }}>
253
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{displayContent}</ReactMarkdown>
254
+ </Box>
255
  ) : (
256
  <Box component="pre" sx={{
257
  m: 0,
frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -72,26 +72,51 @@ export function useAgentWebSocket({
72
  const currentTurnMsgId = useAgentStore.getState().currentTurnMessageId;
73
 
74
  if (currentTurnMsgId) {
75
- // Update existing message - append content and update trace
76
  const messages = useAgentStore.getState().getMessages(sessionId);
77
  const existingMsg = messages.find(m => m.id === currentTurnMsgId);
78
 
79
  if (existingMsg) {
80
- const newContent = existingMsg.content ? existingMsg.content + '\n\n' + content : content;
 
 
 
 
 
 
 
 
 
 
 
 
81
  updateMessage(sessionId, currentTurnMsgId, {
82
- content: newContent,
83
- trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
84
  });
85
  }
86
  } else {
87
  // Create new message
88
  const messageId = `msg_${Date.now()}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  const message: Message = {
90
  id: messageId,
91
  role: 'assistant',
92
  content,
93
  timestamp: new Date().toISOString(),
94
- trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
95
  };
96
  addMessage(sessionId, message);
97
  setCurrentTurnMessageId(messageId);
@@ -112,6 +137,8 @@ export function useAgentWebSocket({
112
  tool: toolName,
113
  timestamp: new Date().toISOString(),
114
  completed: false,
 
 
115
  };
116
  addTraceLog(log);
117
  // Update the current turn message's trace in real-time
@@ -153,26 +180,54 @@ export function useAgentWebSocket({
153
  const output = (event.data?.output as string) || '';
154
  const success = event.data?.success as boolean;
155
 
156
- // Mark the corresponding trace log as completed
157
- updateTraceLog(toolName, { completed: true });
158
  // Update the current turn message's trace in real-time
159
  updateCurrentTurnTrace(sessionId);
160
 
161
- // Special handling for hf_jobs - update the approval message with output
162
  if (toolName === 'hf_jobs') {
163
  const messages = useAgentStore.getState().getMessages(sessionId);
164
- const lastApprovalMsg = [...messages].reverse().find(m => m.approval);
165
-
166
- if (lastApprovalMsg) {
167
- const currentOutput = lastApprovalMsg.toolOutput || '';
168
- const newOutput = currentOutput ? currentOutput + '\n\n' + output : output;
169
-
170
- useAgentStore.getState().updateMessage(sessionId, lastApprovalMsg.id, {
171
- toolOutput: newOutput
172
- });
173
- console.log('Updated approval message with tool output:', toolName);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  } else {
175
- console.warn('Received hf_jobs output but no approval message found to update.');
 
 
 
 
 
 
 
176
  }
177
  }
178
 
@@ -241,6 +296,49 @@ export function useAgentWebSocket({
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
 
 
72
  const currentTurnMsgId = useAgentStore.getState().currentTurnMessageId;
73
 
74
  if (currentTurnMsgId) {
75
+ // Update existing message - add segments chronologically
76
  const messages = useAgentStore.getState().getMessages(sessionId);
77
  const existingMsg = messages.find(m => m.id === currentTurnMsgId);
78
 
79
  if (existingMsg) {
80
+ const segments = existingMsg.segments ? [...existingMsg.segments] : [];
81
+
82
+ // If there are pending traces, add them as a tools segment first
83
+ if (currentTrace.length > 0) {
84
+ segments.push({ type: 'tools', tools: [...currentTrace] });
85
+ clearTraceLogs();
86
+ }
87
+
88
+ // Add the new text segment
89
+ if (content) {
90
+ segments.push({ type: 'text', content });
91
+ }
92
+
93
  updateMessage(sessionId, currentTurnMsgId, {
94
+ content: existingMsg.content + '\n\n' + content,
95
+ segments,
96
  });
97
  }
98
  } else {
99
  // Create new message
100
  const messageId = `msg_${Date.now()}`;
101
+ const segments: Array<{ type: 'text' | 'tools'; content?: string; tools?: typeof currentTrace }> = [];
102
+
103
+ // Add any pending traces first
104
+ if (currentTrace.length > 0) {
105
+ segments.push({ type: 'tools', tools: [...currentTrace] });
106
+ clearTraceLogs();
107
+ }
108
+
109
+ // Add the text
110
+ if (content) {
111
+ segments.push({ type: 'text', content });
112
+ }
113
+
114
  const message: Message = {
115
  id: messageId,
116
  role: 'assistant',
117
  content,
118
  timestamp: new Date().toISOString(),
119
+ segments,
120
  };
121
  addMessage(sessionId, message);
122
  setCurrentTurnMessageId(messageId);
 
137
  tool: toolName,
138
  timestamp: new Date().toISOString(),
139
  completed: false,
140
+ // Store args for auto-exec message creation later
141
+ args: toolName === 'hf_jobs' ? args : undefined,
142
  };
143
  addTraceLog(log);
144
  // Update the current turn message's trace in real-time
 
180
  const output = (event.data?.output as string) || '';
181
  const success = event.data?.success as boolean;
182
 
183
+ // Mark the corresponding trace log as completed and store the output
184
+ updateTraceLog(toolName, { completed: true, output, success });
185
  // Update the current turn message's trace in real-time
186
  updateCurrentTurnTrace(sessionId);
187
 
188
+ // Special handling for hf_jobs - update or create job message with output
189
  if (toolName === 'hf_jobs') {
190
  const messages = useAgentStore.getState().getMessages(sessionId);
191
+ const traceLogs = useAgentStore.getState().traceLogs;
192
+
193
+ // Find existing approval message for this job
194
+ let jobMsg = [...messages].reverse().find(m => m.approval);
195
+
196
+ if (!jobMsg) {
197
+ // No approval message exists - this was an auto-executed job
198
+ // Create a job execution message so user can see results
199
+ const jobTrace = [...traceLogs].reverse().find(t => t.tool === 'hf_jobs');
200
+ const args = jobTrace?.args || {};
201
+
202
+ const autoExecMessage: Message = {
203
+ id: `msg_auto_${Date.now()}`,
204
+ role: 'assistant',
205
+ content: '',
206
+ timestamp: new Date().toISOString(),
207
+ approval: {
208
+ status: 'approved', // Auto-approved (no user action needed)
209
+ batch: {
210
+ tools: [{
211
+ tool: toolName,
212
+ arguments: args,
213
+ tool_call_id: `auto_${Date.now()}`
214
+ }],
215
+ count: 1
216
+ }
217
+ },
218
+ toolOutput: output
219
+ };
220
+ addMessage(sessionId, autoExecMessage);
221
+ console.log('Created auto-exec message with tool output:', toolName);
222
  } else {
223
+ // Update existing approval message
224
+ const currentOutput = jobMsg.toolOutput || '';
225
+ const newOutput = currentOutput ? currentOutput + '\n\n' + output : output;
226
+
227
+ useAgentStore.getState().updateMessage(sessionId, jobMsg.id, {
228
+ toolOutput: newOutput
229
+ });
230
+ console.log('Updated job message with tool output:', toolName);
231
  }
232
  }
233
 
 
296
  };
297
  addMessage(sessionId, message);
298
 
299
+ // Show the first tool's content in the panel so users see what they're approving
300
+ if (tools && tools.length > 0) {
301
+ const firstTool = tools[0];
302
+ const args = firstTool.arguments as Record<string, any>;
303
+
304
+ clearPanelTabs();
305
+
306
+ if (firstTool.tool === 'hf_jobs' && args.script) {
307
+ setPanelTab({
308
+ id: 'script',
309
+ title: 'Script',
310
+ content: args.script,
311
+ language: 'python',
312
+ parameters: args
313
+ });
314
+ setActivePanelTab('script');
315
+ } else if (firstTool.tool === 'hf_repo_files' && args.content) {
316
+ const filename = args.path || 'file';
317
+ const isPython = filename.endsWith('.py');
318
+ setPanelTab({
319
+ id: 'content',
320
+ title: filename.split('/').pop() || 'Content',
321
+ content: args.content,
322
+ language: isPython ? 'python' : 'text',
323
+ parameters: args
324
+ });
325
+ setActivePanelTab('content');
326
+ } else {
327
+ // For other tools, show args as JSON
328
+ setPanelTab({
329
+ id: 'args',
330
+ title: firstTool.tool,
331
+ content: JSON.stringify(args, null, 2),
332
+ language: 'json',
333
+ parameters: args
334
+ });
335
+ setActivePanelTab('args');
336
+ }
337
+
338
+ setRightPanelOpen(true);
339
+ setLeftSidebarOpen(false);
340
+ }
341
+
342
  // Clear currentTurnMessageId so subsequent assistant_message events create a new message below the approval
343
  setCurrentTurnMessageId(null);
344
 
frontend/src/store/agentStore.ts CHANGED
@@ -47,9 +47,11 @@ interface AgentStore {
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;
 
53
  }
54
 
55
  export const useAgentStore = create<AgentStore>((set, get) => ({
@@ -181,6 +183,21 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
181
  set({ panelTabs: [], activePanelTab: null });
182
  },
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  setPlan: (plan: PlanItem[]) => {
185
  set({ plan });
186
  },
@@ -206,4 +223,38 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
206
  });
207
  }
208
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }));
 
47
  setPanelTab: (tab: PanelTab) => void;
48
  setActivePanelTab: (tabId: string) => void;
49
  clearPanelTabs: () => void;
50
+ removePanelTab: (tabId: string) => void;
51
  setPlan: (plan: PlanItem[]) => void;
52
  setCurrentTurnMessageId: (id: string | null) => void;
53
  updateCurrentTurnTrace: (sessionId: string) => void;
54
+ showToolOutput: (log: TraceLog) => void;
55
  }
56
 
57
  export const useAgentStore = create<AgentStore>((set, get) => ({
 
183
  set({ panelTabs: [], activePanelTab: null });
184
  },
185
 
186
+ removePanelTab: (tabId: string) => {
187
+ set((state) => {
188
+ const newTabs = state.panelTabs.filter(t => t.id !== tabId);
189
+ // If we removed the active tab, switch to another tab or null
190
+ let newActiveTab = state.activePanelTab;
191
+ if (state.activePanelTab === tabId) {
192
+ newActiveTab = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
193
+ }
194
+ return {
195
+ panelTabs: newTabs,
196
+ activePanelTab: newActiveTab,
197
+ };
198
+ });
199
+ },
200
+
201
  setPlan: (plan: PlanItem[]) => {
202
  set({ plan });
203
  },
 
223
  });
224
  }
225
  },
226
+
227
+ showToolOutput: (log: TraceLog) => {
228
+ // Show tool output in the right panel - only ONE tool output tab at a time
229
+ const state = get();
230
+
231
+ // Determine language based on content
232
+ let language = 'text';
233
+ const content = log.output || '';
234
+
235
+ // Check if content looks like JSON
236
+ if (content.trim().startsWith('{') || content.trim().startsWith('[') || content.includes('```json')) {
237
+ language = 'json';
238
+ }
239
+ // Check if content has markdown tables or formatting
240
+ else if (content.includes('|') && content.includes('---') || content.includes('```')) {
241
+ language = 'markdown';
242
+ }
243
+
244
+ // Remove any existing tool output tab (only keep one)
245
+ const otherTabs = state.panelTabs.filter(t => t.id !== 'tool_output');
246
+
247
+ // Create/replace the single tool output tab
248
+ const newTab = {
249
+ id: 'tool_output',
250
+ title: log.tool,
251
+ content: content || 'No output available',
252
+ language,
253
+ };
254
+
255
+ set({
256
+ panelTabs: [...otherTabs, newTab],
257
+ activePanelTab: 'tool_output',
258
+ });
259
+ },
260
  }));
frontend/src/types/agent.ts CHANGED
@@ -9,14 +9,18 @@ export interface SessionMeta {
9
  isActive: boolean;
10
  }
11
 
 
 
 
 
 
 
12
  export interface Message {
13
  id: string;
14
  role: 'user' | 'assistant' | 'tool';
15
  content: string;
16
  timestamp: string;
17
- toolName?: string;
18
- tool_call_id?: string;
19
- trace?: TraceLog[];
20
  approval?: {
21
  status: 'pending' | 'approved' | 'rejected';
22
  batch: ApprovalBatch;
@@ -55,6 +59,9 @@ export interface TraceLog {
55
  tool: string;
56
  timestamp: string;
57
  completed?: boolean;
 
 
 
58
  }
59
 
60
  export interface User {
 
9
  isActive: boolean;
10
  }
11
 
12
+ export interface MessageSegment {
13
+ type: 'text' | 'tools';
14
+ content?: string;
15
+ tools?: TraceLog[];
16
+ }
17
+
18
  export interface Message {
19
  id: string;
20
  role: 'user' | 'assistant' | 'tool';
21
  content: string;
22
  timestamp: string;
23
+ segments?: MessageSegment[];
 
 
24
  approval?: {
25
  status: 'pending' | 'approved' | 'rejected';
26
  batch: ApprovalBatch;
 
59
  tool: string;
60
  timestamp: string;
61
  completed?: boolean;
62
+ args?: Record<string, unknown>; // Store args for auto-exec jobs
63
+ output?: string; // Store tool output for display
64
+ success?: boolean; // Whether the tool call succeeded
65
  }
66
 
67
  export interface User {