glaip-sdk 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -85,6 +85,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
85
85
  # Rendering helpers
86
86
  # ------------------------------------------------------------------
87
87
  def _render(self) -> None:
88
+ """Render the transcript viewer interface."""
88
89
  try:
89
90
  if self.console.is_terminal:
90
91
  self.console.clear()
@@ -115,12 +116,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
115
116
  self._render_transcript_view(query)
116
117
 
117
118
  def _render_default_view(self, query: str | None) -> None:
119
+ """Render the default summary view.
120
+
121
+ Args:
122
+ query: Optional user query to display.
123
+ """
118
124
  if query:
119
125
  self._render_user_query(query)
120
126
  self._render_steps_summary()
121
127
  self._render_final_panel()
122
128
 
123
129
  def _render_transcript_view(self, query: str | None) -> None:
130
+ """Render the full transcript view with events.
131
+
132
+ Args:
133
+ query: Optional user query to display.
134
+ """
124
135
  if not self.ctx.events:
125
136
  self.console.print("[dim]No SSE events were captured for this run.[/dim]")
126
137
  return
@@ -148,6 +159,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
148
159
  self.console.print()
149
160
 
150
161
  def _render_final_panel(self) -> None:
162
+ """Render the final result panel."""
151
163
  content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
152
164
  title = "Final Result"
153
165
  duration_text = self._extract_final_duration()
@@ -161,6 +173,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
161
173
  # Interaction loops
162
174
  # ------------------------------------------------------------------
163
175
  def _fallback_loop(self) -> None:
176
+ """Fallback interaction loop for non-interactive terminals."""
164
177
  while True:
165
178
  try:
166
179
  ch = click.getchar()
@@ -181,6 +194,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
181
194
  continue
182
195
 
183
196
  def _handle_command(self, raw: str) -> bool:
197
+ """Handle a command input.
198
+
199
+ Args:
200
+ raw: Raw command string.
201
+
202
+ Returns:
203
+ True to continue, False to exit.
204
+ """
184
205
  lowered = raw.lower()
185
206
  if lowered in {"exit", "quit", "q"}:
186
207
  return True
@@ -339,15 +360,26 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
339
360
  self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
340
361
 
341
362
  def _print_command_hint(self) -> None:
363
+ """Print command hint for user interaction."""
342
364
  self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
343
365
  self.console.print()
344
366
 
345
367
  def _get_user_query(self) -> str | None:
368
+ """Extract user query from metadata or manifest.
369
+
370
+ Returns:
371
+ User query string or None.
372
+ """
346
373
  meta = self.ctx.meta or {}
347
374
  manifest = self.ctx.manifest_entry or {}
348
375
  return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
349
376
 
350
377
  def _render_user_query(self, query: str) -> None:
378
+ """Render user query in a panel.
379
+
380
+ Args:
381
+ query: User query string to render.
382
+ """
351
383
  panel = AIPPanel(
352
384
  Markdown(f"Query: {query}"),
353
385
  title="User Request",
@@ -357,6 +389,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
357
389
  self.console.print()
358
390
 
359
391
  def _render_steps_summary(self) -> None:
392
+ """Render steps summary panel."""
360
393
  stored_lines = self.ctx.meta.get("transcript_step_lines")
361
394
  if stored_lines:
362
395
  body = Text("\n".join(stored_lines), style="dim")
@@ -373,6 +406,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
373
406
 
374
407
  @staticmethod
375
408
  def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
409
+ """Format steps summary as text.
410
+
411
+ Args:
412
+ steps: List of step dictionaries.
413
+
414
+ Returns:
415
+ Formatted text string.
416
+ """
376
417
  if not steps:
377
418
  return " No steps yet"
378
419
 
@@ -388,6 +429,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
388
429
 
389
430
  @staticmethod
390
431
  def _extract_event_time(event: dict[str, Any]) -> float | None:
432
+ """Extract timestamp from event metadata.
433
+
434
+ Args:
435
+ event: Event dictionary.
436
+
437
+ Returns:
438
+ Time value as float or None.
439
+ """
391
440
  metadata = event.get("metadata") or {}
392
441
  time_value = metadata.get("time")
393
442
  try:
@@ -399,6 +448,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
399
448
 
400
449
  @staticmethod
401
450
  def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
451
+ """Parse received timestamp from event.
452
+
453
+ Args:
454
+ event: Event dictionary.
455
+
456
+ Returns:
457
+ Parsed datetime or None.
458
+ """
402
459
  value = event.get("received_at")
403
460
  if not value:
404
461
  return None
@@ -412,6 +469,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
412
469
  return None
413
470
 
414
471
  def _extract_final_duration(self) -> str | None:
472
+ """Extract final duration from events.
473
+
474
+ Returns:
475
+ Duration string or None.
476
+ """
415
477
  for event in self.ctx.events:
416
478
  metadata = event.get("metadata") or {}
417
479
  if metadata.get("kind") == "final_response":
@@ -424,6 +486,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
424
486
  return None
425
487
 
426
488
  def _build_step_summary(self) -> list[dict[str, Any]]:
489
+ """Build step summary from stored steps or events.
490
+
491
+ Returns:
492
+ List of step dictionaries.
493
+ """
427
494
  stored = self.ctx.meta.get("transcript_steps")
428
495
  if isinstance(stored, list) and stored:
429
496
  return [
@@ -496,6 +563,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
496
563
  is_last: bool,
497
564
  lines: list[str],
498
565
  ) -> None:
566
+ """Recursively render a tree branch of steps.
567
+
568
+ Args:
569
+ manager: StepManager instance.
570
+ step_id: ID of step to render.
571
+ ancestor_state: Tuple of ancestor branch states.
572
+ is_last: Whether this is the last child.
573
+ lines: List to append rendered lines to.
574
+ """
499
575
  step = manager.by_id.get(step_id)
500
576
  if not step:
501
577
  return
@@ -525,6 +601,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
525
601
  )
526
602
 
527
603
  def _should_hide_step(self, step: Any) -> bool:
604
+ """Check if a step should be hidden.
605
+
606
+ Args:
607
+ step: Step object.
608
+
609
+ Returns:
610
+ True if step should be hidden.
611
+ """
528
612
  if getattr(step, "parent_id", None) is None:
529
613
  return False
530
614
  name = getattr(step, "name", "") or ""
@@ -536,6 +620,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
536
620
  root_id: str,
537
621
  lines: list[str],
538
622
  ) -> None:
623
+ """Decorate root step presentation with friendly label.
624
+
625
+ Args:
626
+ manager: StepManager instance.
627
+ root_id: Root step ID.
628
+ lines: Lines list to modify.
629
+ """
539
630
  if not lines:
540
631
  return
541
632
 
@@ -554,6 +645,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
554
645
  lines.insert(1, f" {query}")
555
646
 
556
647
  def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
648
+ """Coerce event to step event format.
649
+
650
+ Args:
651
+ event: Event dictionary.
652
+
653
+ Returns:
654
+ Step event dictionary or None.
655
+ """
557
656
  metadata = event.get("metadata")
558
657
  if not isinstance(metadata, dict):
559
658
  return None
@@ -569,6 +668,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
569
668
  }
570
669
 
571
670
  def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
671
+ """Format a tree line for a step.
672
+
673
+ Args:
674
+ step: Step object.
675
+ branch_state: Branch state tuple.
676
+
677
+ Returns:
678
+ Formatted line string.
679
+ """
572
680
  prefix = build_connector_prefix(branch_state)
573
681
  raw_label = normalise_display_label(getattr(step, "display_label", None))
574
682
  title, summary = self._split_label(raw_label)
@@ -594,6 +702,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
594
702
  return line
595
703
 
596
704
  def _friendly_root_label(self, step: Any, fallback: str | None) -> str:
705
+ """Generate friendly label for root step.
706
+
707
+ Args:
708
+ step: Step object.
709
+ fallback: Fallback label string.
710
+
711
+ Returns:
712
+ Friendly label string.
713
+ """
597
714
  agent_name = self.ctx.manifest_entry.get("agent_name") or (self.ctx.meta or {}).get("agent_name")
598
715
  agent_id = self.ctx.manifest_entry.get("agent_id") or getattr(step, "name", "")
599
716
 
@@ -607,6 +724,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
607
724
 
608
725
  @staticmethod
609
726
  def _format_duration_badge(step: Any) -> str | None:
727
+ """Format duration badge for a step.
728
+
729
+ Args:
730
+ step: Step object.
731
+
732
+ Returns:
733
+ Duration badge string or None.
734
+ """
610
735
  duration_ms = getattr(step, "duration_ms", None)
611
736
  if duration_ms is None:
612
737
  return None
@@ -626,6 +751,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
626
751
 
627
752
  @staticmethod
628
753
  def _split_label(label: str) -> tuple[str, str | None]:
754
+ """Split label into title and summary.
755
+
756
+ Args:
757
+ label: Label string.
758
+
759
+ Returns:
760
+ Tuple of (title, summary).
761
+ """
629
762
  if " — " in label:
630
763
  title, summary = label.split(" — ", 1)
631
764
  return title.strip(), summary.strip()
@@ -633,6 +766,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
633
766
 
634
767
  @staticmethod
635
768
  def _truncate_summary(summary: str, limit: int = 48) -> str:
769
+ """Truncate summary to specified length.
770
+
771
+ Args:
772
+ summary: Summary string.
773
+ limit: Maximum length.
774
+
775
+ Returns:
776
+ Truncated summary string.
777
+ """
636
778
  summary = summary.strip()
637
779
  if len(summary) <= limit:
638
780
  return summary
@@ -640,6 +782,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
640
782
 
641
783
  @staticmethod
642
784
  def _looks_like_uuid(value: str) -> bool:
785
+ """Check if string looks like a UUID.
786
+
787
+ Args:
788
+ value: String to check.
789
+
790
+ Returns:
791
+ True if value looks like UUID.
792
+ """
643
793
  stripped = value.replace("-", "").replace(" ", "")
644
794
  if len(stripped) not in {32, 36}:
645
795
  return False
@@ -647,6 +797,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
647
797
 
648
798
  @staticmethod
649
799
  def _format_duration_from_ms(value: Any) -> str | None:
800
+ """Format duration from milliseconds.
801
+
802
+ Args:
803
+ value: Duration value in milliseconds.
804
+
805
+ Returns:
806
+ Formatted duration string or None.
807
+ """
650
808
  try:
651
809
  if value is None:
652
810
  return None
@@ -662,12 +820,29 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
662
820
 
663
821
  @staticmethod
664
822
  def _is_step_event(metadata: dict[str, Any]) -> bool:
823
+ """Check if metadata represents a step event.
824
+
825
+ Args:
826
+ metadata: Event metadata dictionary.
827
+
828
+ Returns:
829
+ True if metadata represents a step event.
830
+ """
665
831
  kind = metadata.get("kind")
666
832
  return kind in {"agent_step", "agent_thinking_step"}
667
833
 
668
834
  def _iter_step_candidates(
669
835
  self, event: dict[str, Any], metadata: dict[str, Any]
670
836
  ) -> Iterable[tuple[str, dict[str, Any]]]:
837
+ """Iterate step candidates from event.
838
+
839
+ Args:
840
+ event: Event dictionary.
841
+ metadata: Event metadata dictionary.
842
+
843
+ Yields:
844
+ Tuples of (step_name, step_info).
845
+ """
671
846
  tool_info = metadata.get("tool_info") or {}
672
847
 
673
848
  yielded = False
@@ -691,6 +866,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
691
866
  def _iter_tool_call_candidates(
692
867
  tool_info: dict[str, Any],
693
868
  ) -> Iterable[tuple[str, dict[str, Any]]]:
869
+ """Iterate tool call candidates from tool_info.
870
+
871
+ Args:
872
+ tool_info: Tool info dictionary.
873
+
874
+ Yields:
875
+ Tuples of (tool_name, tool_call_info).
876
+ """
694
877
  tool_calls = tool_info.get("tool_calls")
695
878
  if isinstance(tool_calls, list):
696
879
  for call in tool_calls:
@@ -702,6 +885,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
702
885
  def _extract_direct_tool(
703
886
  tool_info: dict[str, Any],
704
887
  ) -> tuple[str, dict[str, Any]] | None:
888
+ """Extract direct tool from tool_info.
889
+
890
+ Args:
891
+ tool_info: Tool info dictionary.
892
+
893
+ Returns:
894
+ Tuple of (tool_name, tool_info) or None.
895
+ """
705
896
  if isinstance(tool_info, dict):
706
897
  name = tool_info.get("name")
707
898
  if name:
@@ -710,6 +901,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
710
901
 
711
902
  @staticmethod
712
903
  def _extract_completed_name(event: dict[str, Any]) -> str | None:
904
+ """Extract completed tool name from event content.
905
+
906
+ Args:
907
+ event: Event dictionary.
908
+
909
+ Returns:
910
+ Tool name or None.
911
+ """
713
912
  content = event.get("content") or ""
714
913
  if isinstance(content, str) and content.startswith("Completed "):
715
914
  name = content.replace("Completed ", "").strip()
@@ -723,6 +922,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
723
922
  order: list[str],
724
923
  name: str,
725
924
  ) -> dict[str, Any]:
925
+ """Ensure step entry exists, creating if needed.
926
+
927
+ Args:
928
+ steps: Steps dictionary.
929
+ order: Order list.
930
+ name: Step name.
931
+
932
+ Returns:
933
+ Step dictionary.
934
+ """
726
935
  if name not in steps:
727
936
  steps[name] = {
728
937
  "name": name,
@@ -742,6 +951,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
742
951
  info: dict[str, Any],
743
952
  event: dict[str, Any],
744
953
  ) -> None:
954
+ """Apply update to step from event metadata.
955
+
956
+ Args:
957
+ step: Step dictionary to update.
958
+ metadata: Event metadata.
959
+ info: Step info dictionary.
960
+ event: Event dictionary.
961
+ """
745
962
  status = metadata.get("status")
746
963
  event_time = metadata.get("time")
747
964
 
@@ -760,6 +977,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
760
977
 
761
978
  @staticmethod
762
979
  def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
980
+ """Check if step is finished.
981
+
982
+ Args:
983
+ metadata: Event metadata.
984
+ event: Event dictionary.
985
+
986
+ Returns:
987
+ True if step is finished.
988
+ """
763
989
  status = metadata.get("status")
764
990
  return status == "finished" or bool(event.get("final"))
765
991
 
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import importlib
10
10
  import logging
11
- import os
12
11
  from collections.abc import Callable, Iterable, Iterator
13
12
  from contextlib import contextmanager
14
13
  from typing import Any, Literal
@@ -26,6 +25,7 @@ from glaip_sdk.branding import (
26
25
  WARNING_STYLE,
27
26
  )
28
27
  from glaip_sdk.cli.commands.update import update_command
28
+ from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
29
29
  from glaip_sdk.cli.utils import command_hint, format_command_hint
30
30
  from glaip_sdk.rich_components import AIPPanel
31
31
 
@@ -72,7 +72,7 @@ def _fetch_latest_version(package_name: str) -> str | None:
72
72
 
73
73
  def _should_check_for_updates() -> bool:
74
74
  """Return False when update checks are explicitly disabled."""
75
- return os.getenv("AIP_NO_UPDATE_CHECK") is None
75
+ return UPDATE_CHECK_ENABLED
76
76
 
77
77
 
78
78
  def _build_update_panel(
@@ -244,6 +244,7 @@ def _suppress_library_logging(
244
244
  def _refresh_installed_version(console: Console, ctx: Any) -> None:
245
245
  """Reload runtime metadata after an in-process upgrade."""
246
246
  new_version: str | None = None
247
+ branding_module: Any | None = None
247
248
 
248
249
  try:
249
250
  version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
@@ -252,16 +253,18 @@ def _refresh_installed_version(console: Console, ctx: Any) -> None:
252
253
  _LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
253
254
 
254
255
  try:
255
- branding_module = importlib.import_module("glaip_sdk.branding")
256
+ branding_module = importlib.reload(importlib.import_module("glaip_sdk.branding"))
256
257
  if new_version:
257
258
  branding_module.SDK_VERSION = new_version
258
259
  except Exception as exc: # pragma: no cover - defensive guard
259
260
  _LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
261
+ branding_module = None
260
262
 
261
263
  session = _get_slash_session(ctx)
262
264
  if session and hasattr(session, "refresh_branding"):
263
265
  try:
264
- session.refresh_branding(new_version)
266
+ branding_cls = getattr(branding_module, "AIPBranding", None) if branding_module else None
267
+ session.refresh_branding(new_version, branding_cls=branding_cls)
265
268
  return
266
269
  except Exception as exc: # pragma: no cover - defensive guard
267
270
  _LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
glaip_sdk/cli/utils.py CHANGED
@@ -18,9 +18,10 @@ from pathlib import Path
18
18
  from typing import TYPE_CHECKING, Any, cast
19
19
 
20
20
  import click
21
+ import yaml
21
22
  from rich.console import Console, Group
22
23
  from rich.markdown import Markdown
23
- from rich.pretty import Pretty
24
+ from rich.syntax import Syntax
24
25
 
25
26
  from glaip_sdk import _version as _version_module
26
27
  from glaip_sdk.branding import (
@@ -30,9 +31,40 @@ from glaip_sdk.branding import (
30
31
  SUCCESS_STYLE,
31
32
  WARNING_STYLE,
32
33
  )
34
+ from glaip_sdk.cli import masking, pager
35
+ from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
36
+ from glaip_sdk.cli.config import load_config
37
+ from glaip_sdk.cli.context import (
38
+ _get_view,
39
+ detect_export_format as _detect_export_format,
40
+ get_ctx_value,
41
+ )
33
42
  from glaip_sdk.cli.rich_helpers import markup_text
34
43
  from glaip_sdk.icons import ICON_AGENT
35
- from glaip_sdk.rich_components import AIPPanel
44
+ from glaip_sdk.rich_components import AIPPanel, AIPTable
45
+ from glaip_sdk.utils import is_uuid
46
+ from glaip_sdk.utils.rendering.renderer import (
47
+ CapturingConsole,
48
+ RendererConfig,
49
+ RichStreamRenderer,
50
+ )
51
+
52
+
53
+ class _LiteralYamlDumper(yaml.SafeDumper):
54
+ """YAML dumper that emits literal scalars for multiline strings."""
55
+
56
+
57
+ def _literal_str_representer(dumper: yaml.Dumper, data: str) -> yaml.nodes.ScalarNode:
58
+ """Represent strings in YAML, using literal blocks for verbose values."""
59
+ needs_literal = "\n" in data or "\r" in data
60
+ if not needs_literal and LITERAL_STRING_THRESHOLD and len(data) >= LITERAL_STRING_THRESHOLD: # pragma: no cover
61
+ needs_literal = True
62
+
63
+ style = "|" if needs_literal else None
64
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
65
+
66
+
67
+ _LiteralYamlDumper.add_representer(str, _literal_str_representer)
36
68
 
37
69
  # Optional interactive deps (fuzzy palette)
38
70
  try:
@@ -56,22 +88,6 @@ except Exception: # pragma: no cover - optional dependency
56
88
 
57
89
  if TYPE_CHECKING: # pragma: no cover - import-only during type checking
58
90
  from glaip_sdk import Client
59
- from glaip_sdk.cli import masking, pager
60
- from glaip_sdk.cli.config import load_config
61
- from glaip_sdk.cli.context import (
62
- _get_view,
63
- get_ctx_value,
64
- )
65
- from glaip_sdk.cli.context import (
66
- detect_export_format as _detect_export_format,
67
- )
68
- from glaip_sdk.rich_components import AIPTable
69
- from glaip_sdk.utils import is_uuid
70
- from glaip_sdk.utils.rendering.renderer import (
71
- CapturingConsole,
72
- RendererConfig,
73
- RichStreamRenderer,
74
- )
75
91
 
76
92
  console = Console()
77
93
  pager.console = console
@@ -602,6 +618,7 @@ def _prompt_with_auto_select(
602
618
  valid_choices = set(choices)
603
619
 
604
620
  def _auto_select(_: Buffer) -> None:
621
+ """Auto-select text when a valid choice is entered."""
605
622
  text = buffer.text
606
623
  if not text or text not in valid_choices:
607
624
  return
@@ -635,9 +652,23 @@ class _FuzzyCompleter:
635
652
  """Fuzzy completer for prompt_toolkit."""
636
653
 
637
654
  def __init__(self, words: list[str]) -> None:
655
+ """Initialize fuzzy completer with word list.
656
+
657
+ Args:
658
+ words: List of words to complete from.
659
+ """
638
660
  self.words = words
639
661
 
640
662
  def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
663
+ """Get fuzzy completions for the current word.
664
+
665
+ Args:
666
+ document: Document object from prompt_toolkit.
667
+ _complete_event: Completion event (unused).
668
+
669
+ Yields:
670
+ Completion objects matching the current word.
671
+ """
641
672
  word = document.get_word_before_cursor()
642
673
  if not word:
643
674
  return
@@ -769,7 +800,7 @@ def _fuzzy_score(search: str, target: str) -> int:
769
800
  return score
770
801
 
771
802
 
772
- # ----------------------------- Pretty outputs ---------------------------- #
803
+ # ----------------------- Structured renderer helpers -------------------- #
773
804
 
774
805
 
775
806
  def _coerce_result_payload(result: Any) -> Any:
@@ -811,6 +842,37 @@ def _render_markdown_output(data: Any) -> None:
811
842
  click.echo(str(data))
812
843
 
813
844
 
845
+ def _format_yaml_text(data: Any) -> str:
846
+ """Convert structured payloads to YAML for readability."""
847
+ try:
848
+ yaml_text = yaml.dump(
849
+ data,
850
+ sort_keys=False,
851
+ default_flow_style=False,
852
+ allow_unicode=True,
853
+ Dumper=_LiteralYamlDumper,
854
+ )
855
+ except Exception: # pragma: no cover - defensive YAML fallback
856
+ try:
857
+ return str(data)
858
+ except Exception: # pragma: no cover - defensive str fallback
859
+ return repr(data)
860
+
861
+ yaml_text = yaml_text.rstrip()
862
+ if yaml_text.endswith("..."): # pragma: no cover - defensive YAML cleanup
863
+ yaml_text = yaml_text[:-3].rstrip()
864
+ return yaml_text
865
+
866
+
867
+ def _build_yaml_renderable(data: Any) -> Any:
868
+ """Return a syntax-highlighted YAML renderable when possible."""
869
+ yaml_text = _format_yaml_text(data) or "# No data"
870
+ try:
871
+ return Syntax(yaml_text, "yaml", word_wrap=False)
872
+ except Exception: # pragma: no cover - defensive syntax highlighting fallback
873
+ return yaml_text
874
+
875
+
814
876
  def output_result(
815
877
  ctx: Any,
816
878
  result: Any,
@@ -843,7 +905,7 @@ def output_result(
843
905
  _render_markdown_output(data)
844
906
  return
845
907
 
846
- renderable = Pretty(data)
908
+ renderable = _build_yaml_renderable(data)
847
909
  if panel_title:
848
910
  console.print(AIPPanel(renderable, title=panel_title))
849
911
  else:
@@ -854,7 +916,7 @@ def output_result(
854
916
  # ----------------------------- List rendering ---------------------------- #
855
917
 
856
918
  # Threshold no longer used - fuzzy palette is always default for TTY
857
- # _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
919
+ # _PICK_THRESHOLD = 5
858
920
 
859
921
 
860
922
  def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
@@ -902,12 +964,7 @@ def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[
902
964
 
903
965
  def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
904
966
  """Return True when rows should be name-sorted prior to rendering."""
905
- return (
906
- os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
907
- and rows
908
- and isinstance(rows[0], dict)
909
- and "name" in rows[0]
910
- )
967
+ return TABLE_SORT_ENABLED and rows and isinstance(rows[0], dict) and "name" in rows[0]
911
968
 
912
969
 
913
970
  def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any: