akseljoonas HF Staff commited on
Commit
767d102
Β·
1 Parent(s): af6a7ab

Overhaul CLI visual design with rich-powered formatting

Browse files

- Replace ASCII banner with compact rich Panel
- Remove init noise (token loaded, MCP servers, etc.)
- Replace "Turn complete" banner with subtle separator
- Tool calls display as clean `β–Έ name args`
- Rich-themed help command with syntax highlighting
- Centralize all display functions in terminal_display module
- Remove Colors class dependency from reliability_checks

agent/main.py CHANGED
@@ -25,14 +25,22 @@ from agent.core.session import OpType
25
  from agent.core.tools import ToolRouter
26
  from agent.utils.reliability_checks import check_training_script_save_pattern
27
  from agent.utils.terminal_display import (
28
- format_error,
29
- format_header,
30
- format_plan_display,
31
- format_separator,
32
- format_success,
33
- format_tool_call,
34
- format_tool_output,
35
- format_turn_complete,
 
 
 
 
 
 
 
 
36
  )
37
 
38
  litellm.drop_params = True
@@ -136,15 +144,8 @@ class Submission:
136
 
137
 
138
  def _create_rich_console():
139
- """Create a rich Console for markdown rendering."""
140
- from rich.console import Console
141
- return Console(highlight=False)
142
-
143
-
144
- def _render_markdown(console, text: str) -> None:
145
- """Render markdown text to the terminal via rich."""
146
- from rich.markdown import Markdown
147
- console.print(Markdown(text))
148
 
149
 
150
  class _ThinkingShimmer:
@@ -266,99 +267,84 @@ async def event_listener(
266
  config=None,
267
  ) -> None:
268
  """Background task that listens for events and displays them"""
269
- submission_id = [1000] # Use list to make it mutable in closure
270
- last_tool_name = [None] # Track last tool called
271
  console = _create_rich_console()
272
- spinner = _ThinkingShimmer(console)
273
  stream_buf = _StreamBuffer(console)
274
 
275
  while True:
276
  try:
277
  event = await event_queue.get()
278
 
279
- # Display event
280
  if event.event_type == "ready":
281
- print(format_success("\U0001f917 Agent ready"))
282
  ready_event.set()
283
  elif event.event_type == "assistant_message":
284
- # Non-streaming: full message arrives at once
285
- spinner.stop()
286
  content = event.data.get("content", "") if event.data else ""
287
  if content:
288
- console.print()
289
- _render_markdown(console, content)
290
  elif event.event_type == "assistant_chunk":
291
- spinner.stop()
292
  content = event.data.get("content", "") if event.data else ""
293
  if content:
294
  stream_buf.add_chunk(content)
295
  elif event.event_type == "assistant_stream_end":
296
  stream_buf.finish()
297
  elif event.event_type == "tool_call":
298
- spinner.stop()
299
  stream_buf.discard()
300
  tool_name = event.data.get("tool", "") if event.data else ""
301
  arguments = event.data.get("arguments", {}) if event.data else {}
302
  if tool_name:
303
- last_tool_name[0] = tool_name # Store for tool_output event
304
- args_str = json.dumps(arguments)[:100] + "..."
305
- print(format_tool_call(tool_name, args_str))
306
  elif event.event_type == "tool_output":
307
  output = event.data.get("output", "") if event.data else ""
308
  success = event.data.get("success", False) if event.data else False
309
  if output:
310
- # Don't truncate plan_tool output, truncate everything else
311
  should_truncate = last_tool_name[0] != "plan_tool"
312
- print(format_tool_output(output, success, truncate=should_truncate))
313
- # After tool output, agent will think again
314
- spinner.start()
315
  elif event.event_type == "turn_complete":
316
- spinner.stop()
317
  stream_buf.discard()
318
- print(format_turn_complete())
319
- # Display plan after turn complete
320
- plan_display = format_plan_display()
321
- if plan_display:
322
- print(plan_display)
323
  turn_complete_event.set()
324
  elif event.event_type == "interrupted":
325
- spinner.stop()
326
  stream_buf.discard()
327
- print("\n(interrupted)")
328
  turn_complete_event.set()
329
  elif event.event_type == "undo_complete":
330
- print("Undo complete.")
331
  turn_complete_event.set()
332
  elif event.event_type == "tool_log":
333
  tool = event.data.get("tool", "") if event.data else ""
334
  log = event.data.get("log", "") if event.data else ""
335
  if log:
336
- print(f" [{tool}] {log}")
337
  elif event.event_type == "tool_state_change":
338
- tool = event.data.get("tool", "") if event.data else ""
339
- state = event.data.get("state", "") if event.data else ""
340
- if state in ("approved", "rejected", "running"):
341
- print(f" {tool}: {state}")
342
  elif event.event_type == "error":
343
- spinner.stop()
344
  stream_buf.discard()
345
- error = (
346
- event.data.get("error", "Unknown error")
347
- if event.data
348
- else "Unknown error"
349
- )
350
- print(format_error(error))
351
  turn_complete_event.set()
352
  elif event.event_type == "shutdown":
353
- spinner.stop()
354
  stream_buf.discard()
355
  break
356
  elif event.event_type == "processing":
357
- spinner.start()
358
  elif event.event_type == "compacted":
359
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
360
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
361
- print(f"Compacted context: {old_tokens} -> {new_tokens} tokens")
362
  elif event.event_type == "approval_required":
363
  # Handle batch approval format
364
  tools_data = event.data.get("tools", []) if event.data else []
@@ -374,7 +360,7 @@ async def event_listener(
374
  }
375
  for t in tools_data
376
  ]
377
- print(f"\n YOLO MODE: Auto-approving {count} item(s)")
378
  submission_id[0] += 1
379
  approval_submission = Submission(
380
  id=f"approval_{submission_id[0]}",
@@ -386,14 +372,7 @@ async def event_listener(
386
  await submission_queue.put(approval_submission)
387
  continue
388
 
389
- print("\n" + format_separator())
390
- print(
391
- format_header(
392
- f"APPROVAL REQUIRED ({count} item{'s' if count != 1 else ''})"
393
- )
394
- )
395
- print(format_separator())
396
-
397
  approvals = []
398
 
399
  # Ask for approval for each tool
@@ -412,9 +391,7 @@ async def event_listener(
412
 
413
  operation = arguments.get("operation", "")
414
 
415
- print(f"\n[Item {i}/{count}]")
416
- print(f"Tool: {tool_name}")
417
- print(f"Operation: {operation}")
418
 
419
  # Handle different tool types
420
  if tool_name == "hf_jobs":
@@ -659,7 +636,7 @@ async def event_listener(
659
  ),
660
  )
661
  await submission_queue.put(approval_submission)
662
- print(format_separator() + "\n")
663
  # Silently ignore other events
664
 
665
  except asyncio.CancelledError:
@@ -677,16 +654,7 @@ async def get_user_input(prompt_session: PromptSession) -> str:
677
 
678
  # ── Slash command helpers ────────────────────────────────────────────────
679
 
680
- HELP_TEXT = """\
681
- Commands:
682
- /help Show this help
683
- /undo Undo last turn
684
- /compact Compact context window
685
- /model [id] Show available models or switch model
686
- /yolo Toggle auto-approve mode
687
- /status Show current model, turn count
688
- /quit, /exit Exit the CLI
689
- """
690
 
691
 
692
  def _handle_slash_command(
@@ -705,7 +673,7 @@ def _handle_slash_command(
705
  arg = parts[1].strip() if len(parts) > 1 else ""
706
 
707
  if command == "/help":
708
- print(HELP_TEXT)
709
  return None
710
 
711
  if command == "/undo":
@@ -764,35 +732,18 @@ def _handle_slash_command(
764
 
765
  async def main():
766
  """Interactive chat with the agent"""
767
- from agent.utils.terminal_display import Colors
768
 
769
  # Clear screen
770
  os.system("clear" if os.name != "nt" else "cls")
771
 
772
- banner = r"""
773
- _ _ _ _____ _ _
774
- | | | |_ _ __ _ __ _(_)_ __ __ _ | ___|_ _ ___ ___ / \ __ _ ___ _ __ | |_
775
- | |_| | | | |/ _` |/ _` | | '_ \ / _` | | |_ / _` |/ __/ _ \ / _ \ / _` |/ _ \ '_ \| __|
776
- | _ | |_| | (_| | (_| | | | | | (_| | | _| (_| | (_| __/ / ___ \ (_| | __/ | | | |_
777
- |_| |_|\__,_|\__, |\__, |_|_| |_|\__, | |_| \__,_|\___\___| /_/ \_\__, |\___|_| |_|\__|
778
- |___/ |___/ |___/ |___/
779
- """
780
-
781
- print(format_separator())
782
- print(f"{Colors.YELLOW} {banner}{Colors.RESET}")
783
- print("Type your messages below. Type /help for commands, /quit to exit.\n")
784
- print(format_separator())
785
- # Wait for agent to initialize
786
- print("Initializing agent...")
787
 
788
  # Create prompt session for input (needed early for token prompt)
789
  prompt_session = PromptSession()
790
 
791
  # HF token β€” required, prompt if missing
792
  hf_token = _get_hf_token()
793
- if hf_token:
794
- print("HF token loaded")
795
- else:
796
  hf_token = await _prompt_and_save_hf_token(prompt_session)
797
 
798
  # Create queues for communication
@@ -809,7 +760,6 @@ async def main():
809
  config = load_config(config_path)
810
 
811
  # Create tool router with local mode
812
- print(f"Loading MCP servers: {', '.join(config.mcpServers.keys())}")
813
  tool_router = ToolRouter(config.mcpServers, hf_token=hf_token, local_mode=True)
814
 
815
  # Session holder for interrupt/model/status access
@@ -911,7 +861,6 @@ async def main():
911
  print("\n\nInterrupted by user")
912
 
913
  # Shutdown
914
- print("\nShutting down agent...")
915
  shutdown_submission = Submission(
916
  id="sub_shutdown", operation=Operation(op_type=OpType.SHUTDOWN)
917
  )
@@ -929,7 +878,7 @@ async def main():
929
  # Now safe to cancel the listener (agent is done emitting events)
930
  listener_task.cancel()
931
 
932
- print("Goodbye!\n")
933
 
934
 
935
  async def headless_main(prompt: str, model: str | None = None) -> None:
@@ -993,58 +942,56 @@ async def headless_main(prompt: str, model: str | None = None) -> None:
993
 
994
  # Process events until turn completes
995
  console = _create_rich_console()
996
- err_console = _create_rich_console()
997
- err_console.file = sys.stderr
998
- spinner = _ThinkingShimmer(console)
999
  stream_buf = _StreamBuffer(console)
1000
- spinner.start()
1001
 
1002
  while True:
1003
  event = await event_queue.get()
1004
 
1005
  if event.event_type == "assistant_chunk":
1006
- spinner.stop()
1007
  content = event.data.get("content", "") if event.data else ""
1008
  if content:
1009
  stream_buf.add_chunk(content)
1010
  elif event.event_type == "assistant_stream_end":
1011
  stream_buf.finish()
1012
  elif event.event_type == "assistant_message":
1013
- spinner.stop()
1014
  content = event.data.get("content", "") if event.data else ""
1015
  if content:
1016
- _render_markdown(console, content)
1017
  elif event.event_type == "tool_call":
1018
- spinner.stop()
1019
  stream_buf.discard()
1020
  tool_name = event.data.get("tool", "") if event.data else ""
1021
  arguments = event.data.get("arguments", {}) if event.data else {}
1022
  if tool_name:
1023
- args_str = json.dumps(arguments)[:100] + "..."
1024
- print(format_tool_call(tool_name, args_str), file=sys.stderr)
1025
  elif event.event_type == "tool_output":
1026
  output = event.data.get("output", "") if event.data else ""
1027
  success = event.data.get("success", False) if event.data else False
1028
  if output:
1029
- print(format_tool_output(output, success, truncate=True), file=sys.stderr)
1030
- spinner.start()
1031
  elif event.event_type == "tool_log":
1032
  tool = event.data.get("tool", "") if event.data else ""
1033
  log = event.data.get("log", "") if event.data else ""
1034
  if log:
1035
- print(f" [{tool}] {log}", file=sys.stderr)
1036
  elif event.event_type == "compacted":
1037
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
1038
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
1039
- print(f"Compacted: {old_tokens} -> {new_tokens} tokens", file=sys.stderr)
1040
  elif event.event_type == "error":
1041
- spinner.stop()
1042
  stream_buf.discard()
1043
  error = event.data.get("error", "Unknown error") if event.data else "Unknown error"
1044
- print(f"ERROR: {error}", file=sys.stderr)
1045
  break
1046
  elif event.event_type in ("turn_complete", "interrupted"):
1047
- spinner.stop()
1048
  stream_buf.discard()
1049
  break
1050
 
 
25
  from agent.core.tools import ToolRouter
26
  from agent.utils.reliability_checks import check_training_script_save_pattern
27
  from agent.utils.terminal_display import (
28
+ get_console,
29
+ print_approval_header,
30
+ print_approval_item,
31
+ print_banner,
32
+ print_compacted,
33
+ print_error,
34
+ print_help,
35
+ print_init_done,
36
+ print_interrupted,
37
+ print_markdown,
38
+ print_plan,
39
+ print_tool_call,
40
+ print_tool_log,
41
+ print_tool_output,
42
+ print_turn_complete,
43
+ print_yolo_approve,
44
  )
45
 
46
  litellm.drop_params = True
 
144
 
145
 
146
  def _create_rich_console():
147
+ """Get the shared rich Console."""
148
+ return get_console()
 
 
 
 
 
 
 
149
 
150
 
151
  class _ThinkingShimmer:
 
267
  config=None,
268
  ) -> None:
269
  """Background task that listens for events and displays them"""
270
+ submission_id = [1000]
271
+ last_tool_name = [None]
272
  console = _create_rich_console()
273
+ shimmer = _ThinkingShimmer(console)
274
  stream_buf = _StreamBuffer(console)
275
 
276
  while True:
277
  try:
278
  event = await event_queue.get()
279
 
 
280
  if event.event_type == "ready":
281
+ print_init_done()
282
  ready_event.set()
283
  elif event.event_type == "assistant_message":
284
+ shimmer.stop()
 
285
  content = event.data.get("content", "") if event.data else ""
286
  if content:
287
+ print_markdown(content)
 
288
  elif event.event_type == "assistant_chunk":
289
+ shimmer.stop()
290
  content = event.data.get("content", "") if event.data else ""
291
  if content:
292
  stream_buf.add_chunk(content)
293
  elif event.event_type == "assistant_stream_end":
294
  stream_buf.finish()
295
  elif event.event_type == "tool_call":
296
+ shimmer.stop()
297
  stream_buf.discard()
298
  tool_name = event.data.get("tool", "") if event.data else ""
299
  arguments = event.data.get("arguments", {}) if event.data else {}
300
  if tool_name:
301
+ last_tool_name[0] = tool_name
302
+ args_str = json.dumps(arguments)[:80]
303
+ print_tool_call(tool_name, args_str)
304
  elif event.event_type == "tool_output":
305
  output = event.data.get("output", "") if event.data else ""
306
  success = event.data.get("success", False) if event.data else False
307
  if output:
 
308
  should_truncate = last_tool_name[0] != "plan_tool"
309
+ print_tool_output(output, success, truncate=should_truncate)
310
+ shimmer.start()
 
311
  elif event.event_type == "turn_complete":
312
+ shimmer.stop()
313
  stream_buf.discard()
314
+ print_turn_complete()
315
+ print_plan()
 
 
 
316
  turn_complete_event.set()
317
  elif event.event_type == "interrupted":
318
+ shimmer.stop()
319
  stream_buf.discard()
320
+ print_interrupted()
321
  turn_complete_event.set()
322
  elif event.event_type == "undo_complete":
323
+ console.print("[dim]Undone.[/dim]")
324
  turn_complete_event.set()
325
  elif event.event_type == "tool_log":
326
  tool = event.data.get("tool", "") if event.data else ""
327
  log = event.data.get("log", "") if event.data else ""
328
  if log:
329
+ print_tool_log(tool, log)
330
  elif event.event_type == "tool_state_change":
331
+ pass # visual noise β€” approval flow handles this
 
 
 
332
  elif event.event_type == "error":
333
+ shimmer.stop()
334
  stream_buf.discard()
335
+ error = event.data.get("error", "Unknown error") if event.data else "Unknown error"
336
+ print_error(error)
 
 
 
 
337
  turn_complete_event.set()
338
  elif event.event_type == "shutdown":
339
+ shimmer.stop()
340
  stream_buf.discard()
341
  break
342
  elif event.event_type == "processing":
343
+ shimmer.start()
344
  elif event.event_type == "compacted":
345
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
346
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
347
+ print_compacted(old_tokens, new_tokens)
348
  elif event.event_type == "approval_required":
349
  # Handle batch approval format
350
  tools_data = event.data.get("tools", []) if event.data else []
 
360
  }
361
  for t in tools_data
362
  ]
363
+ print_yolo_approve(count)
364
  submission_id[0] += 1
365
  approval_submission = Submission(
366
  id=f"approval_{submission_id[0]}",
 
372
  await submission_queue.put(approval_submission)
373
  continue
374
 
375
+ print_approval_header(count)
 
 
 
 
 
 
 
376
  approvals = []
377
 
378
  # Ask for approval for each tool
 
391
 
392
  operation = arguments.get("operation", "")
393
 
394
+ print_approval_item(i, count, tool_name, operation)
 
 
395
 
396
  # Handle different tool types
397
  if tool_name == "hf_jobs":
 
636
  ),
637
  )
638
  await submission_queue.put(approval_submission)
639
+ console.print() # spacing after approval
640
  # Silently ignore other events
641
 
642
  except asyncio.CancelledError:
 
654
 
655
  # ── Slash command helpers ────────────────────────────────────────────────
656
 
657
+ # Slash commands are defined in terminal_display
 
 
 
 
 
 
 
 
 
658
 
659
 
660
  def _handle_slash_command(
 
673
  arg = parts[1].strip() if len(parts) > 1 else ""
674
 
675
  if command == "/help":
676
+ print_help()
677
  return None
678
 
679
  if command == "/undo":
 
732
 
733
  async def main():
734
  """Interactive chat with the agent"""
 
735
 
736
  # Clear screen
737
  os.system("clear" if os.name != "nt" else "cls")
738
 
739
+ print_banner()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
  # Create prompt session for input (needed early for token prompt)
742
  prompt_session = PromptSession()
743
 
744
  # HF token β€” required, prompt if missing
745
  hf_token = _get_hf_token()
746
+ if not hf_token:
 
 
747
  hf_token = await _prompt_and_save_hf_token(prompt_session)
748
 
749
  # Create queues for communication
 
760
  config = load_config(config_path)
761
 
762
  # Create tool router with local mode
 
763
  tool_router = ToolRouter(config.mcpServers, hf_token=hf_token, local_mode=True)
764
 
765
  # Session holder for interrupt/model/status access
 
861
  print("\n\nInterrupted by user")
862
 
863
  # Shutdown
 
864
  shutdown_submission = Submission(
865
  id="sub_shutdown", operation=Operation(op_type=OpType.SHUTDOWN)
866
  )
 
878
  # Now safe to cancel the listener (agent is done emitting events)
879
  listener_task.cancel()
880
 
881
+ get_console().print("\n[dim]Bye.[/dim]\n")
882
 
883
 
884
  async def headless_main(prompt: str, model: str | None = None) -> None:
 
942
 
943
  # Process events until turn completes
944
  console = _create_rich_console()
945
+ shimmer = _ThinkingShimmer(console)
 
 
946
  stream_buf = _StreamBuffer(console)
947
+ shimmer.start()
948
 
949
  while True:
950
  event = await event_queue.get()
951
 
952
  if event.event_type == "assistant_chunk":
953
+ shimmer.stop()
954
  content = event.data.get("content", "") if event.data else ""
955
  if content:
956
  stream_buf.add_chunk(content)
957
  elif event.event_type == "assistant_stream_end":
958
  stream_buf.finish()
959
  elif event.event_type == "assistant_message":
960
+ shimmer.stop()
961
  content = event.data.get("content", "") if event.data else ""
962
  if content:
963
+ print_markdown(content)
964
  elif event.event_type == "tool_call":
965
+ shimmer.stop()
966
  stream_buf.discard()
967
  tool_name = event.data.get("tool", "") if event.data else ""
968
  arguments = event.data.get("arguments", {}) if event.data else {}
969
  if tool_name:
970
+ args_str = json.dumps(arguments)[:80]
971
+ print_tool_call(tool_name, args_str)
972
  elif event.event_type == "tool_output":
973
  output = event.data.get("output", "") if event.data else ""
974
  success = event.data.get("success", False) if event.data else False
975
  if output:
976
+ print_tool_output(output, success, truncate=True)
977
+ shimmer.start()
978
  elif event.event_type == "tool_log":
979
  tool = event.data.get("tool", "") if event.data else ""
980
  log = event.data.get("log", "") if event.data else ""
981
  if log:
982
+ print_tool_log(tool, log)
983
  elif event.event_type == "compacted":
984
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
985
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
986
+ print_compacted(old_tokens, new_tokens)
987
  elif event.event_type == "error":
988
+ shimmer.stop()
989
  stream_buf.discard()
990
  error = event.data.get("error", "Unknown error") if event.data else "Unknown error"
991
+ print_error(error)
992
  break
993
  elif event.event_type in ("turn_complete", "interrupted"):
994
+ shimmer.stop()
995
  stream_buf.discard()
996
  break
997
 
agent/utils/reliability_checks.py CHANGED
@@ -1,7 +1,5 @@
1
  """Reliability checks for job submissions and other operations"""
2
 
3
- from agent.utils.terminal_display import Colors
4
-
5
 
6
  def check_training_script_save_pattern(script: str) -> str | None:
7
  """Check if a training script properly saves models."""
@@ -9,8 +7,8 @@ def check_training_script_save_pattern(script: str) -> str | None:
9
  has_push_to_hub = "push_to_hub" in script
10
 
11
  if has_from_pretrained and not has_push_to_hub:
12
- return f"\n{Colors.RED}WARNING: We've detected that no model will be saved at the end of this training script. Please ensure this is what you want.{Colors.RESET}"
13
  elif has_from_pretrained and has_push_to_hub:
14
- return f"\n{Colors.GREEN}We've detected that a model will be pushed to hub at the end of this training.{Colors.RESET}"
15
 
16
  return None
 
1
  """Reliability checks for job submissions and other operations"""
2
 
 
 
3
 
4
  def check_training_script_save_pattern(script: str) -> str | None:
5
  """Check if a training script properly saves models."""
 
7
  has_push_to_hub = "push_to_hub" in script
8
 
9
  if has_from_pretrained and not has_push_to_hub:
10
+ return "\n\033[91mWARNING: No model save detected in this script. Ensure this is intentional.\033[0m"
11
  elif has_from_pretrained and has_push_to_hub:
12
+ return "\n\033[92mModel will be pushed to hub after training.\033[0m"
13
 
14
  return None
agent/utils/terminal_display.py CHANGED
@@ -1,155 +1,191 @@
1
  """
2
- Terminal display utilities with colors and formatting
3
  """
4
 
 
 
 
 
 
5
 
6
- # ANSI color codes
7
- class Colors:
8
- RED = "\033[91m"
9
- GREEN = "\033[92m"
10
- YELLOW = "\033[93m"
11
- BLUE = "\033[94m"
12
- MAGENTA = "\033[95m"
13
- CYAN = "\033[96m"
14
- BOLD = "\033[1m"
15
- UNDERLINE = "\033[4m"
16
- RESET = "\033[0m"
17
 
 
18
 
19
- def truncate_to_lines(text: str, max_lines: int = 6) -> str:
20
- """Truncate text to max_lines, adding '...' if truncated"""
21
- lines = text.split("\n")
22
- if len(lines) <= max_lines:
23
- return text
24
- return (
25
- "\n".join(lines[:max_lines])
26
- + f"\n{Colors.CYAN}... ({len(lines) - max_lines} more lines){Colors.RESET}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  )
 
28
 
29
 
30
- def format_header(text: str, emoji: str = "") -> str:
31
- """Format a header with bold"""
32
- full_text = f"{emoji} {text}" if emoji else text
33
- return f"{Colors.BOLD}{full_text}{Colors.RESET}"
34
 
 
 
35
 
36
- def format_plan_display() -> str:
37
- """Format the current plan for display (no colors, full visibility)"""
38
- from agent.tools.plan_tool import get_current_plan
39
 
40
- plan = get_current_plan()
41
- if not plan:
42
- return ""
43
 
44
- lines = ["\n" + "=" * 60]
45
- lines.append("CURRENT PLAN")
46
- lines.append("=" * 60 + "\n")
47
 
48
- # Group by status
49
- completed = [t for t in plan if t["status"] == "completed"]
50
- in_progress = [t for t in plan if t["status"] == "in_progress"]
51
- pending = [t for t in plan if t["status"] == "pending"]
52
 
53
- if completed:
54
- lines.append("Completed:")
55
- for todo in completed:
56
- lines.append(f" [x] {todo['id']}. {todo['content']}")
57
- lines.append("")
58
-
59
- if in_progress:
60
- lines.append("In Progress:")
61
- for todo in in_progress:
62
- lines.append(f" [~] {todo['id']}. {todo['content']}")
63
- lines.append("")
64
-
65
- if pending:
66
- lines.append("Pending:")
67
- for todo in pending:
68
- lines.append(f" [ ] {todo['id']}. {todo['content']}")
69
- lines.append("")
70
-
71
- lines.append(
72
- f"Total: {len(plan)} todos ({len(completed)} completed, {len(in_progress)} in progress, {len(pending)} pending)"
73
- )
74
- lines.append("=" * 60 + "\n")
75
 
76
- return "\n".join(lines)
77
 
 
 
78
 
79
- def format_error(message: str) -> str:
80
- """Format an error message in red"""
81
- return f"{Colors.RED}ERROR: {message}{Colors.RESET}"
82
 
 
83
 
84
- def format_success(message: str, emoji: str = "") -> str:
85
- """Format a success message in green"""
86
- prefix = f"{emoji} " if emoji else ""
87
- return f"{Colors.GREEN}{prefix}{message}{Colors.RESET}"
88
 
89
 
90
- def format_tool_call(tool_name: str, arguments: str) -> str:
91
- """Format a tool call message"""
92
- return f"{Colors.YELLOW}Calling tool: {Colors.BOLD}{tool_name}{Colors.RESET}{Colors.YELLOW} with arguments: {arguments}{Colors.RESET}"
93
 
94
 
95
- def format_tool_output(output: str, success: bool, truncate: bool = True) -> str:
96
- """Format tool output with color and optional truncation"""
97
- original_length = len(output)
98
- if truncate:
99
- output = truncate_to_lines(output, max_lines=6)
100
 
101
- if success:
102
- return (
103
- f"{Colors.YELLOW}Tool output ({original_length} tkns): {Colors.RESET}\n{output}"
104
- )
105
- else:
106
- return (
107
- f"{Colors.RED}Tool output ({original_length} tokens): {Colors.RESET}\n{output}"
108
- )
 
 
 
 
 
 
 
 
 
 
 
109
 
110
 
111
- def format_turn_complete() -> str:
112
- """Format turn complete message in green with hugging face emoji"""
113
- return f"{Colors.GREEN}{Colors.BOLD}\U0001f917 Turn complete{Colors.RESET}\n"
114
 
115
 
116
- def format_separator(char: str = "=", length: int = 60) -> str:
117
- """Format a separator line"""
118
- return char * length
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  def format_plan_tool_output(todos: list) -> str:
122
- """Format the plan tool output (no colors, full visibility)"""
123
  if not todos:
124
  return "Plan is empty."
125
 
126
- lines = ["Plan updated successfully", ""]
127
-
128
- # Group by status
129
  completed = [t for t in todos if t["status"] == "completed"]
130
  in_progress = [t for t in todos if t["status"] == "in_progress"]
131
  pending = [t for t in todos if t["status"] == "pending"]
132
 
133
- if completed:
134
- lines.append("Completed:")
135
- for todo in completed:
136
- lines.append(f" [x] {todo['id']}. {todo['content']}")
137
- lines.append("")
138
-
139
- if in_progress:
140
- lines.append("In Progress:")
141
- for todo in in_progress:
142
- lines.append(f" [~] {todo['id']}. {todo['content']}")
143
- lines.append("")
144
-
145
- if pending:
146
- lines.append("Pending:")
147
- for todo in pending:
148
- lines.append(f" [ ] {todo['id']}. {todo['content']}")
149
- lines.append("")
150
-
151
- lines.append(
152
- f"Total: {len(todos)} todos ({len(completed)} completed, {len(in_progress)} in progress, {len(pending)} pending)"
153
- )
154
 
 
155
  return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Terminal display utilities β€” rich-powered CLI formatting.
3
  """
4
 
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+ from rich.theme import Theme
10
 
11
+ _THEME = Theme({
12
+ "tool.name": "bold cyan",
13
+ "tool.args": "dim",
14
+ "tool.ok": "dim green",
15
+ "tool.fail": "dim red",
16
+ "info": "dim",
17
+ "muted": "dim",
18
+ })
 
 
 
19
 
20
+ _console = Console(theme=_THEME, highlight=False)
21
 
22
+
23
+ def get_console() -> Console:
24
+ return _console
25
+
26
+
27
+ # ── Banner ─────────────────────────────────────────────────────────────
28
+
29
+ def print_banner() -> None:
30
+ logo = Text.from_ansi(
31
+ "\033[38;2;255;200;50m" # warm gold
32
+ " πŸ€— Hugging Face Agent\n"
33
+ "\033[0m"
34
+ )
35
+ _console.print()
36
+ _console.print(
37
+ Panel(
38
+ logo,
39
+ subtitle="[dim]/help for commands Β· /quit to exit[/dim]",
40
+ border_style="dim",
41
+ expand=False,
42
+ padding=(0, 2),
43
+ )
44
  )
45
+ _console.print()
46
 
47
 
48
+ # ── Init progress ──────────────────────────────────────────────────────
 
 
 
49
 
50
+ def print_init_done() -> None:
51
+ _console.print("[dim]Ready.[/dim]\n")
52
 
 
 
 
53
 
54
+ # ── Tool calls ─────────────────────────────────────────────────────────
 
 
55
 
56
+ def print_tool_call(tool_name: str, args_preview: str) -> None:
57
+ _console.print(f" [tool.name]β–Έ {tool_name}[/tool.name] [tool.args]{args_preview}[/tool.args]")
 
58
 
 
 
 
 
59
 
60
+ def print_tool_output(output: str, success: bool, truncate: bool = True) -> None:
61
+ if truncate:
62
+ output = _truncate(output, max_lines=6)
63
+ style = "tool.ok" if success else "tool.fail"
64
+ _console.print(f" [{style}]{output}[/{style}]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
 
66
 
67
+ def print_tool_log(tool: str, log: str) -> None:
68
+ _console.print(f" [dim]{tool}:[/dim] [dim]{log}[/dim]")
69
 
 
 
 
70
 
71
+ # ── Messages ───────────────────────────────────────────────────────────
72
 
73
+ def print_markdown(text: str) -> None:
74
+ _console.print()
75
+ _console.print(Markdown(text))
 
76
 
77
 
78
+ def print_error(message: str) -> None:
79
+ _console.print(f"\n[bold red]Error:[/bold red] {message}")
 
80
 
81
 
82
+ def print_turn_complete() -> None:
83
+ # Subtle separator β€” no noisy "turn complete" banner
84
+ _console.print("[dim]─[/dim]")
 
 
85
 
86
+
87
+ def print_interrupted() -> None:
88
+ _console.print("\n[dim italic]interrupted[/dim italic]")
89
+
90
+
91
+ def print_compacted(old_tokens: int, new_tokens: int) -> None:
92
+ _console.print(f" [dim]context compacted: {old_tokens:,} β†’ {new_tokens:,} tokens[/dim]")
93
+
94
+
95
+ # ── Approval ───────────────────────────────────────────────────────────
96
+
97
+ def print_approval_header(count: int) -> None:
98
+ label = f"Approval required β€” {count} item{'s' if count != 1 else ''}"
99
+ _console.print()
100
+ _console.print(Panel(f"[bold yellow]{label}[/bold yellow]", border_style="yellow", expand=False))
101
+
102
+
103
+ def print_approval_item(index: int, total: int, tool_name: str, operation: str) -> None:
104
+ _console.print(f"\n [bold]\\[{index}/{total}][/bold] [tool.name]{tool_name}[/tool.name] {operation}")
105
 
106
 
107
+ def print_yolo_approve(count: int) -> None:
108
+ _console.print(f" [bold yellow]yolo β†’[/bold yellow] auto-approved {count} item(s)")
 
109
 
110
 
111
+ # ── Help ───────────────────────────────────────────────────────────────
 
 
112
 
113
+ HELP_TEXT = """\
114
+ [bold]Commands[/bold]
115
+ [cyan]/help[/cyan] Show this help
116
+ [cyan]/undo[/cyan] Undo last turn
117
+ [cyan]/compact[/cyan] Compact context window
118
+ [cyan]/model[/cyan] [id] Show available models or switch
119
+ [cyan]/yolo[/cyan] Toggle auto-approve mode
120
+ [cyan]/status[/cyan] Current model & turn count
121
+ [cyan]/quit[/cyan] Exit"""
122
+
123
+
124
+ def print_help() -> None:
125
+ _console.print()
126
+ _console.print(HELP_TEXT)
127
+ _console.print()
128
+
129
+
130
+ # ── Plan display ───────────────────────────────────────────────────────
131
+
132
+ def format_plan_display() -> str:
133
+ """Format the current plan for display."""
134
+ from agent.tools.plan_tool import get_current_plan
135
+
136
+ plan = get_current_plan()
137
+ if not plan:
138
+ return ""
139
+
140
+ completed = [t for t in plan if t["status"] == "completed"]
141
+ in_progress = [t for t in plan if t["status"] == "in_progress"]
142
+ pending = [t for t in plan if t["status"] == "pending"]
143
+
144
+ lines = []
145
+ for t in completed:
146
+ lines.append(f" [green]βœ“[/green] [dim]{t['content']}[/dim]")
147
+ for t in in_progress:
148
+ lines.append(f" [yellow]β–Έ[/yellow] {t['content']}")
149
+ for t in pending:
150
+ lines.append(f" [dim]β—‹ {t['content']}[/dim]")
151
+
152
+ summary = f"[dim]{len(completed)}/{len(plan)} done[/dim]"
153
+ lines.append(f" {summary}")
154
+ return "\n".join(lines)
155
+
156
+
157
+ def print_plan() -> None:
158
+ plan_str = format_plan_display()
159
+ if plan_str:
160
+ _console.print(plan_str)
161
+
162
+
163
+ # ── Formatting for plan_tool output (used by plan_tool handler) ────────
164
 
165
  def format_plan_tool_output(todos: list) -> str:
 
166
  if not todos:
167
  return "Plan is empty."
168
 
169
+ lines = ["Plan updated:", ""]
 
 
170
  completed = [t for t in todos if t["status"] == "completed"]
171
  in_progress = [t for t in todos if t["status"] == "in_progress"]
172
  pending = [t for t in todos if t["status"] == "pending"]
173
 
174
+ for t in completed:
175
+ lines.append(f" [x] {t['id']}. {t['content']}")
176
+ for t in in_progress:
177
+ lines.append(f" [~] {t['id']}. {t['content']}")
178
+ for t in pending:
179
+ lines.append(f" [ ] {t['id']}. {t['content']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ lines.append(f"\n{len(completed)}/{len(todos)} done")
182
  return "\n".join(lines)
183
+
184
+
185
+ # ── Internal helpers ───────────────────────────────────────────────────
186
+
187
+ def _truncate(text: str, max_lines: int = 6) -> str:
188
+ lines = text.split("\n")
189
+ if len(lines) <= max_lines:
190
+ return text
191
+ return "\n".join(lines[:max_lines]) + f"\n... ({len(lines) - max_lines} more lines)"