@@ -116,13 +116,24 @@ def build_execution_tree(raw: object, depth: int = 0) -> dict[str, object] | Non
116116 if isinstance (prompt , str ):
117117 prompt_preview = _truncate (prompt )
118118 elif isinstance (prompt , dict ):
119- prompt_preview = _truncate (str (prompt ))
119+ # Try to extract meaningful preview from context dict
120+ if "query" in prompt :
121+ prompt_preview = _truncate (str (prompt ["query" ]))
122+ elif "root" in prompt :
123+ prompt_preview = f"[context: { prompt .get ('root' , 'unknown' )} ]"
124+ else :
125+ prompt_preview = _truncate (str (prompt ), 60 )
120126 elif isinstance (prompt , list ) and prompt :
121127 # Message list - get last user message content
122128 for msg in reversed (prompt ):
123129 if isinstance (msg , dict ) and msg .get ("role" ) == "user" :
124130 content = msg .get ("content" , "" )
125- prompt_preview = _truncate (str (content ))
131+ if isinstance (content , str ):
132+ prompt_preview = _truncate (content )
133+ elif isinstance (content , dict ) and "query" in content :
134+ prompt_preview = _truncate (str (content ["query" ]))
135+ else :
136+ prompt_preview = _truncate (str (content ))
126137 break
127138
128139 node : dict [str , object ] = {
@@ -192,6 +203,78 @@ def _build_iteration_node(iteration: object, num: int, depth: int) -> dict[str,
192203 return node
193204
194205
206+ def render_execution_tree (raw : object ) -> str | None :
207+ """
208+ Render execution tree as ASCII art for terminal display.
209+
210+ Returns a string like:
211+ ┌─ [openai/gpt-4] 2.3s $0.05
212+ │ Q: Analyze the codebase...
213+ │ A: I'll start by...
214+ │
215+ └─┬─ [google/gemini] 1.2s $0.02
216+ │ Q: What is X?
217+ │ A: X is...
218+ │
219+ └── [openai/gpt-4] 0.5s
220+ Q: Details?
221+ A: The details...
222+ """
223+ tree = build_execution_tree (raw )
224+ if tree is None :
225+ return None
226+
227+ lines : list [str ] = []
228+
229+ def format_node_header (node : dict [str , object ]) -> str :
230+ model = node .get ("model" , "unknown" )
231+ duration = node .get ("duration" , 0 )
232+ cost = node .get ("cost" )
233+ header = f"[{ model } ] { duration } s"
234+ if cost :
235+ header += f" ${ cost :.4f} "
236+ return header
237+
238+ def render_node (node : dict [str , object ], prefix : str , is_last : bool , is_root : bool ) -> None :
239+ # Determine box drawing characters
240+ if is_root :
241+ branch = "┌─ "
242+ child_prefix = "│ "
243+ elif is_last :
244+ branch = "└── "
245+ child_prefix = " "
246+ else :
247+ branch = "├── "
248+ child_prefix = "│ "
249+
250+ # Node header
251+ header = format_node_header (node )
252+ lines .append (f"{ prefix } { branch } { header } " )
253+
254+ # Content prefix for Q/A lines
255+ content_prefix = prefix + child_prefix
256+
257+ # Show prompt/response previews
258+ prompt = node .get ("prompt_preview" , "" )
259+ response = node .get ("response_preview" , "" )
260+ if prompt :
261+ lines .append (f"{ content_prefix } Q: { prompt } " )
262+ if response :
263+ lines .append (f"{ content_prefix } A: { response } " )
264+
265+ # Process children
266+ children = node .get ("children" , []) or []
267+ if children :
268+ lines .append (content_prefix .rstrip ()) # blank line before children
269+ for i , child in enumerate (children ):
270+ if isinstance (child , dict ):
271+ is_last_child = i == len (children ) - 1
272+ render_node (child , content_prefix , is_last_child , False )
273+
274+ render_node (tree , "" , True , True )
275+ return "\n " .join (lines )
276+
277+
195278def build_execution_summary (raw : object ) -> dict [str , object ] | None :
196279 """
197280 Build summary statistics from execution tree.
0 commit comments