Henri Bonamy commited on
Commit
4485208
·
1 Parent(s): 29d492e

plan tool bottom right

Browse files
agent/tools/jobs_tool.py CHANGED
@@ -367,17 +367,48 @@ class HfJobsTool:
367
 
368
  for _ in range(max_retries):
369
  try:
370
- # Fetch logs - generator streams logs as they arrive
371
- logs_gen = self.api.fetch_job_logs(job_id=job_id, namespace=namespace)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
- # Stream logs in real-time
374
- for log_line in logs_gen:
375
  print("\t" + log_line)
376
  if self.log_callback:
377
  await self.log_callback(log_line)
378
  all_logs.append(log_line)
379
 
380
- # If we get here, streaming completed normally
 
 
381
  break
382
 
383
  except (
 
367
 
368
  for _ in range(max_retries):
369
  try:
370
+ # Use a queue to bridge sync generator to async consumer
371
+ queue = asyncio.Queue()
372
+ loop = asyncio.get_running_loop()
373
+
374
+ def log_producer():
375
+ try:
376
+ # fetch_job_logs is a blocking sync generator
377
+ logs_gen = self.api.fetch_job_logs(job_id=job_id, namespace=namespace)
378
+ for line in logs_gen:
379
+ # Push line to queue thread-safely
380
+ loop.call_soon_threadsafe(queue.put_nowait, line)
381
+ # Signal EOF
382
+ loop.call_soon_threadsafe(queue.put_nowait, None)
383
+ except Exception as e:
384
+ # Signal error
385
+ loop.call_soon_threadsafe(queue.put_nowait, e)
386
+
387
+ # Start producer in a background thread so it doesn't block the event loop
388
+ producer_future = loop.run_in_executor(None, log_producer)
389
+
390
+ # Consume logs from the queue as they arrive
391
+ while True:
392
+ item = await queue.get()
393
+
394
+ # EOF sentinel
395
+ if item is None:
396
+ break
397
+
398
+ # Error occurred in producer
399
+ if isinstance(item, Exception):
400
+ raise item
401
 
402
+ # Process log line
403
+ log_line = item
404
  print("\t" + log_line)
405
  if self.log_callback:
406
  await self.log_callback(log_line)
407
  all_logs.append(log_line)
408
 
409
+ # If we get here, streaming completed normally (EOF received)
410
+ # Wait for thread to cleanup (should be done)
411
+ await producer_future
412
  break
413
 
414
  except (
agent/tools/plan_tool.py CHANGED
@@ -1,5 +1,6 @@
1
  from typing import Any, Dict, List
2
 
 
3
  from agent.utils.terminal_display import format_plan_tool_output
4
 
5
  from .types import ToolResult
@@ -11,8 +12,8 @@ _current_plan: List[Dict[str, str]] = []
11
  class PlanTool:
12
  """Tool for managing a list of todos with status tracking."""
13
 
14
- def __init__(self):
15
- pass
16
 
17
  async def execute(self, params: Dict[str, Any]) -> ToolResult:
18
  """
@@ -56,6 +57,15 @@ class PlanTool:
56
  # Store the raw todos structure in memory
57
  _current_plan = todos
58
 
 
 
 
 
 
 
 
 
 
59
  # Format only for display using terminal_display utility
60
  formatted_output = format_plan_tool_output(todos)
61
 
@@ -120,7 +130,9 @@ PLAN_TOOL_SPEC = {
120
  }
121
 
122
 
123
- async def plan_tool_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
124
- tool = PlanTool()
 
 
125
  result = await tool.execute(arguments)
126
  return result["formatted"], not result.get("isError", False)
 
1
  from typing import Any, Dict, List
2
 
3
+ from agent.core.session import Event
4
  from agent.utils.terminal_display import format_plan_tool_output
5
 
6
  from .types import ToolResult
 
12
  class PlanTool:
13
  """Tool for managing a list of todos with status tracking."""
14
 
15
+ def __init__(self, session: Any = None):
16
+ self.session = session
17
 
18
  async def execute(self, params: Dict[str, Any]) -> ToolResult:
19
  """
 
57
  # Store the raw todos structure in memory
58
  _current_plan = todos
59
 
60
+ # Emit plan update event if session is available
61
+ if self.session:
62
+ await self.session.send_event(
63
+ Event(
64
+ event_type="plan_update",
65
+ data={"plan": todos},
66
+ )
67
+ )
68
+
69
  # Format only for display using terminal_display utility
70
  formatted_output = format_plan_tool_output(todos)
71
 
 
130
  }
131
 
132
 
133
+ async def plan_tool_handler(
134
+ arguments: Dict[str, Any], session: Any = None
135
+ ) -> tuple[str, bool]:
136
+ tool = PlanTool(session=session)
137
  result = await tool.execute(arguments)
138
  return result["formatted"], not result.get("isError", False)
frontend/src/components/CodePanel/CodePanel.tsx CHANGED
@@ -1,12 +1,15 @@
1
- import { Box, Typography, IconButton } from '@mui/material';
2
  import CloseIcon from '@mui/icons-material/Close';
 
 
 
3
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
  import { useAgentStore } from '@/store/agentStore';
6
  import { useLayoutStore } from '@/store/layoutStore';
7
 
8
  export default function CodePanel() {
9
- const { panelContent } = useAgentStore();
10
  const { setRightPanelOpen } = useLayoutStore();
11
 
12
  return (
@@ -28,64 +31,107 @@ export default function CodePanel() {
28
  </IconButton>
29
  </Box>
30
 
31
- {!panelContent ? (
32
- <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
33
- <Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}>
34
- NO DATA LOADED
35
- </Typography>
36
- </Box>
37
- ) : (
38
- <Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
39
- <Box
40
- className="code-panel"
41
- sx={{
42
- background: '#0A0B0C',
43
- borderRadius: 'var(--radius-md)',
44
- padding: '18px',
45
- border: '1px solid rgba(255,255,255,0.03)',
46
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
47
- fontSize: '13px',
48
- lineHeight: 1.55,
49
- height: '100%',
50
- overflow: 'auto',
51
- }}
52
- >
53
- {panelContent.content ? (
54
- panelContent.language === 'python' ? (
55
- <SyntaxHighlighter
56
- language="python"
57
- style={vscDarkPlus}
58
- customStyle={{
59
- margin: 0,
60
- padding: 0,
61
- background: 'transparent',
62
  fontSize: '13px',
63
- fontFamily: 'inherit',
64
- }}
65
- wrapLines={true}
66
- wrapLongLines={true}
67
- >
68
- {panelContent.content}
69
- </SyntaxHighlighter>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  ) : (
71
- <Box component="pre" sx={{
72
- m: 0,
73
- fontFamily: 'inherit',
74
- color: 'var(--text)',
75
- whiteSpace: 'pre-wrap',
76
- wordBreak: 'break-all'
77
- }}>
78
- <code>{panelContent.content}</code>
79
- </Box>
80
- )
81
- ) : (
82
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
83
- <Typography variant="caption">
84
- NO CONTENT TO DISPLAY
 
 
 
 
 
 
 
 
 
85
  </Typography>
86
- </Box>
87
- )}
88
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </Box>
90
  )}
91
  </Box>
 
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';
5
+ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
6
  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 (
 
31
  </IconButton>
32
  </Box>
33
 
34
+ {/* Main Content Area */}
35
+ <Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
36
+ {!panelContent ? (
37
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
38
+ <Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}>
39
+ NO DATA LOADED
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',
48
+ borderRadius: 'var(--radius-md)',
49
+ padding: '18px',
50
+ border: '1px solid rgba(255,255,255,0.03)',
51
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  fontSize: '13px',
53
+ lineHeight: 1.55,
54
+ height: '100%',
55
+ overflow: 'auto',
56
+ }}
57
+ >
58
+ {panelContent.content ? (
59
+ panelContent.language === 'python' ? (
60
+ <SyntaxHighlighter
61
+ language="python"
62
+ style={vscDarkPlus}
63
+ customStyle={{
64
+ margin: 0,
65
+ padding: 0,
66
+ background: 'transparent',
67
+ fontSize: '13px',
68
+ fontFamily: 'inherit',
69
+ }}
70
+ wrapLines={true}
71
+ wrapLongLines={true}
72
+ >
73
+ {panelContent.content}
74
+ </SyntaxHighlighter>
75
+ ) : (
76
+ <Box component="pre" sx={{
77
+ m: 0,
78
+ fontFamily: 'inherit',
79
+ color: 'var(--text)',
80
+ whiteSpace: 'pre-wrap',
81
+ wordBreak: 'break-all'
82
+ }}>
83
+ <code>{panelContent.content}</code>
84
+ </Box>
85
+ )
86
  ) : (
87
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
88
+ <Typography variant="caption">
89
+ NO CONTENT TO DISPLAY
90
+ </Typography>
91
+ </Box>
92
+ )}
93
+ </Box>
94
+ </Box>
95
+ )}
96
+ </Box>
97
+
98
+ {/* Plan Display at Bottom */}
99
+ {plan && plan.length > 0 && (
100
+ <Box sx={{
101
+ borderTop: '1px solid rgba(255,255,255,0.03)',
102
+ bgcolor: 'rgba(0,0,0,0.2)',
103
+ maxHeight: '30%',
104
+ display: 'flex',
105
+ flexDirection: 'column'
106
+ }}>
107
+ <Box sx={{ p: 1.5, borderBottom: '1px solid rgba(255,255,255,0.03)', display: 'flex', alignItems: 'center', gap: 1 }}>
108
+ <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
109
+ CURRENT PLAN
110
  </Typography>
111
+ </Box>
112
+ <Box sx={{ p: 2, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 1 }}>
113
+ {plan.map((item) => (
114
+ <Box key={item.id} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
115
+ <Box sx={{ mt: 0.2 }}>
116
+ {item.status === 'completed' && <CheckCircleIcon sx={{ fontSize: 16, color: 'var(--accent-green)' }} />}
117
+ {item.status === 'in_progress' && <PlayCircleOutlineIcon sx={{ fontSize: 16, color: 'var(--accent-yellow)' }} />}
118
+ {item.status === 'pending' && <RadioButtonUncheckedIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.5 }} />}
119
+ </Box>
120
+ <Typography
121
+ variant="body2"
122
+ sx={{
123
+ fontSize: '13px',
124
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
125
+ color: item.status === 'completed' ? 'var(--muted-text)' : 'var(--text)',
126
+ textDecoration: item.status === 'completed' ? 'line-through' : 'none',
127
+ opacity: item.status === 'pending' ? 0.7 : 1
128
+ }}
129
+ >
130
+ {item.content}
131
+ </Typography>
132
+ </Box>
133
+ ))}
134
+ </Box>
135
  </Box>
136
  )}
137
  </Box>
frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -32,6 +32,7 @@ export function useAgentWebSocket({
32
  addTraceLog,
33
  clearTraceLogs,
34
  setPanelContent,
 
35
  traceLogs,
36
  } = useAgentStore();
37
 
@@ -148,6 +149,15 @@ export function useAgentWebSocket({
148
  break;
149
  }
150
 
 
 
 
 
 
 
 
 
 
151
  case 'approval_required': {
152
  const tools = event.data?.tools as Array<{
153
  tool: string;
 
32
  addTraceLog,
33
  clearTraceLogs,
34
  setPanelContent,
35
+ setPlan,
36
  traceLogs,
37
  } = useAgentStore();
38
 
 
149
  break;
150
  }
151
 
152
+ case 'plan_update': {
153
+ const plan = (event.data?.plan as any[]) || [];
154
+ setPlan(plan);
155
+ if (!useLayoutStore.getState().isRightPanelOpen) {
156
+ setRightPanelOpen(true);
157
+ }
158
+ break;
159
+ }
160
+
161
  case 'approval_required': {
162
  const tools = event.data?.tools as Array<{
163
  tool: string;
frontend/src/store/agentStore.ts CHANGED
@@ -1,6 +1,12 @@
1
  import { create } from 'zustand';
2
  import type { Message, ApprovalBatch, User, TraceLog } from '@/types/agent';
3
 
 
 
 
 
 
 
4
  interface AgentStore {
5
  // State per session (keyed by session ID)
6
  messagesBySession: Record<string, Message[]>;
@@ -11,6 +17,7 @@ interface AgentStore {
11
  error: string | null;
12
  traceLogs: TraceLog[];
13
  panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
 
14
 
15
  // Actions
16
  addMessage: (sessionId: string, message: Message) => void;
@@ -24,6 +31,7 @@ interface AgentStore {
24
  addTraceLog: (log: TraceLog) => void;
25
  clearTraceLogs: () => void;
26
  setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
 
27
  }
28
 
29
  export const useAgentStore = create<AgentStore>((set, get) => ({
@@ -35,6 +43,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
35
  error: null,
36
  traceLogs: [],
37
  panelContent: null,
 
38
 
39
  addMessage: (sessionId: string, message: Message) => {
40
  set((state) => {
@@ -94,4 +103,8 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
94
  setPanelContent: (content) => {
95
  set({ panelContent: content });
96
  },
 
 
 
 
97
  }));
 
1
  import { create } from 'zustand';
2
  import type { Message, ApprovalBatch, User, TraceLog } from '@/types/agent';
3
 
4
+ export interface PlanItem {
5
+ id: string;
6
+ content: string;
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
  error: string | null;
18
  traceLogs: TraceLog[];
19
  panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
20
+ plan: PlanItem[];
21
 
22
  // Actions
23
  addMessage: (sessionId: string, message: Message) => void;
 
31
  addTraceLog: (log: TraceLog) => void;
32
  clearTraceLogs: () => void;
33
  setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
34
+ setPlan: (plan: PlanItem[]) => void;
35
  }
36
 
37
  export const useAgentStore = create<AgentStore>((set, get) => ({
 
43
  error: null,
44
  traceLogs: [],
45
  panelContent: null,
46
+ plan: [],
47
 
48
  addMessage: (sessionId: string, message: Message) => {
49
  set((state) => {
 
103
  setPanelContent: (content) => {
104
  set({ panelContent: content });
105
  },
106
+
107
+ setPlan: (plan: PlanItem[]) => {
108
+ set({ plan });
109
+ },
110
  }));
frontend/src/types/events.ts CHANGED
@@ -15,7 +15,8 @@ export type EventType =
15
  | 'error'
16
  | 'shutdown'
17
  | 'interrupted'
18
- | 'undo_complete';
 
19
 
20
  export interface AgentEvent {
21
  event_type: EventType;
@@ -50,6 +51,10 @@ export interface ToolLogEventData {
50
  log: string;
51
  }
52
 
 
 
 
 
53
  export interface ApprovalRequiredEventData {
54
  tools: ApprovalToolItem[];
55
  count: number;
 
15
  | 'error'
16
  | 'shutdown'
17
  | 'interrupted'
18
+ | 'undo_complete'
19
+ | 'plan_update';
20
 
21
  export interface AgentEvent {
22
  event_type: EventType;
 
51
  log: string;
52
  }
53
 
54
+ export interface PlanUpdateEventData {
55
+ plan: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed' }>;
56
+ }
57
+
58
  export interface ApprovalRequiredEventData {
59
  tools: ApprovalToolItem[];
60
  count: number;