glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -1 +1,115 @@
1
- """Rendering utilities package (formatting, models, steps, debug)."""
1
+ """Rendering utilities package (formatting, models, steps, debug).
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+ from glaip_sdk.models.agent_runs import RunWithOutput
13
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event, render_debug_event_stream
14
+
15
+
16
+ def _parse_event_received_timestamp(event: dict[str, Any]) -> datetime | None:
17
+ """Parse received_at timestamp from SSE event.
18
+
19
+ Args:
20
+ event: SSE event dictionary
21
+
22
+ Returns:
23
+ Parsed datetime or None if not available
24
+ """
25
+ received_at = event.get("received_at")
26
+ if not received_at:
27
+ return None
28
+
29
+ if isinstance(received_at, datetime):
30
+ return received_at
31
+
32
+ if isinstance(received_at, str):
33
+ try:
34
+ # Try ISO format first
35
+ return datetime.fromisoformat(received_at.replace("Z", "+00:00"))
36
+ except ValueError:
37
+ try:
38
+ # Try common formats
39
+ return datetime.strptime(received_at, "%Y-%m-%dT%H:%M:%S.%fZ")
40
+ except ValueError:
41
+ return None
42
+
43
+ return None
44
+
45
+
46
+ def render_remote_sse_transcript(
47
+ run: RunWithOutput,
48
+ console: Console,
49
+ *,
50
+ show_metadata: bool = True,
51
+ ) -> None:
52
+ """Render remote SSE transcript events for a RunWithOutput.
53
+
54
+ Args:
55
+ run: RunWithOutput instance containing events
56
+ console: Rich console to render to
57
+ show_metadata: Whether to show run metadata summary
58
+ """
59
+ if show_metadata:
60
+ # Render metadata summary
61
+ console.print(f"[bold]Run: {run.id}[/bold]")
62
+ console.print(f"[dim]Agent: {run.agent_id}[/dim]")
63
+ console.print(f"[dim]Status: {run.status}[/dim]")
64
+ console.print(f"[dim]Type: {run.run_type}[/dim]")
65
+ if run.schedule_id:
66
+ console.print(f"[dim]Schedule ID: {run.schedule_id}[/dim]")
67
+ else:
68
+ console.print("[dim]Schedule: —[/dim]")
69
+ console.print(f"[dim]Started: {run.started_at.isoformat()}[/dim]")
70
+ if run.completed_at:
71
+ console.print(f"[dim]Completed: {run.completed_at.isoformat()}[/dim]")
72
+ console.print(f"[dim]Duration: {run.duration_formatted()}[/dim]")
73
+ console.print()
74
+
75
+ # Render events
76
+ if not run.output:
77
+ console.print("[dim]No SSE events available for this run.[/dim]")
78
+ return
79
+
80
+ console.print("[bold]SSE Events[/bold]")
81
+ console.print("[dim]────────────────────────────────────────────────────────[/dim]")
82
+
83
+ render_debug_event_stream(
84
+ run.output,
85
+ console,
86
+ resolve_timestamp=_parse_event_received_timestamp,
87
+ )
88
+ console.print()
89
+
90
+
91
+ class RemoteSSETranscriptRenderer:
92
+ """Renderer for remote SSE transcripts from RunWithOutput."""
93
+
94
+ def __init__(self, console: Console | None = None):
95
+ """Initialize the renderer.
96
+
97
+ Args:
98
+ console: Rich console instance (creates default if None)
99
+ """
100
+ self.console = console or Console()
101
+
102
+ def render(self, run: RunWithOutput, *, show_metadata: bool = True) -> None:
103
+ """Render a remote run transcript.
104
+
105
+ Args:
106
+ run: RunWithOutput instance to render
107
+ show_metadata: Whether to show run metadata summary
108
+ """
109
+ render_remote_sse_transcript(run, self.console, show_metadata=show_metadata)
110
+
111
+
112
+ __all__ = [
113
+ "render_remote_sse_transcript",
114
+ "RemoteSSETranscriptRenderer",
115
+ ]
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import json
10
10
  import re
11
- import time
12
11
  from collections.abc import Callable
13
12
  from typing import Any
14
13
 
@@ -47,11 +46,6 @@ SENSITIVE_PATTERNS = re.compile(
47
46
  r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
48
47
  re.IGNORECASE,
49
48
  )
50
- CONNECTOR_VERTICAL = "│ "
51
- CONNECTOR_EMPTY = " "
52
- CONNECTOR_BRANCH = "├─ "
53
- CONNECTOR_LAST = "└─ "
54
- ROOT_MARKER = ""
55
49
  SECRET_MASK = "••••••"
56
50
  STATUS_GLYPHS = {
57
51
  "success": ICON_STATUS_SUCCESS,
@@ -140,20 +134,11 @@ def glyph_for_status(icon_key: str | None) -> str | None:
140
134
 
141
135
  def normalise_display_label(label: str | None) -> str:
142
136
  """Return a user facing label or the Unknown fallback."""
143
- label = (label or "").strip()
144
- return label or "Unknown step detail"
145
-
146
-
147
- def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
148
- """Build connector prefix for a tree line based on ancestry state."""
149
- if not branch_state:
150
- return ROOT_MARKER
151
-
152
- parts: list[str] = []
153
- for ancestor_is_last in branch_state[:-1]:
154
- parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
155
- parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
156
- return "".join(parts)
137
+ if not isinstance(label, str):
138
+ text = ""
139
+ else:
140
+ text = label.strip()
141
+ return text or "Unknown step detail"
157
142
 
158
143
 
159
144
  def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
@@ -202,16 +187,6 @@ def pretty_out(output: any, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
202
187
  return _truncate_string(output_str, max_len)
203
188
 
204
189
 
205
- def get_spinner_char() -> str:
206
- """Get the next character for a spinner animation.
207
-
208
- Returns:
209
- A single character from the spinner frames based on current time
210
- """
211
- frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
212
- return frames[int(time.time() * 10) % len(frames)]
213
-
214
-
215
190
  def get_step_icon(step_kind: str) -> str:
216
191
  """Get the appropriate icon for a step kind."""
217
192
  if step_kind == "tool":
@@ -0,0 +1,64 @@
1
+ """Layout utilities exposed for renderer/viewer consumers.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.utils.rendering.layout.panels import (
8
+ create_context_panel,
9
+ create_final_panel,
10
+ create_main_panel,
11
+ create_tool_panel,
12
+ )
13
+ from glaip_sdk.utils.rendering.layout.progress import (
14
+ TrailingSpinnerLine,
15
+ build_progress_footer,
16
+ format_elapsed_time,
17
+ format_tool_title,
18
+ format_working_indicator,
19
+ get_spinner,
20
+ get_spinner_char,
21
+ is_delegation_tool,
22
+ )
23
+ from glaip_sdk.utils.rendering.layout.transcript import (
24
+ DEFAULT_TRANSCRIPT_THEME,
25
+ TranscriptGlyphs,
26
+ TranscriptRow,
27
+ TranscriptSnapshot,
28
+ build_final_panel,
29
+ build_transcript_snapshot,
30
+ build_transcript_view,
31
+ extract_query_from_meta,
32
+ format_final_panel_title,
33
+ render_final_panel,
34
+ )
35
+ from glaip_sdk.utils.rendering.layout.summary import render_summary_panels
36
+
37
+ __all__ = [
38
+ # Panels
39
+ "create_context_panel",
40
+ "create_final_panel",
41
+ "create_main_panel",
42
+ "create_tool_panel",
43
+ "render_summary_panels",
44
+ # Progress
45
+ "TrailingSpinnerLine",
46
+ "build_progress_footer",
47
+ "format_elapsed_time",
48
+ "format_tool_title",
49
+ "format_working_indicator",
50
+ "get_spinner",
51
+ "get_spinner_char",
52
+ "is_delegation_tool",
53
+ # Transcript
54
+ "DEFAULT_TRANSCRIPT_THEME",
55
+ "TranscriptGlyphs",
56
+ "TranscriptRow",
57
+ "TranscriptSnapshot",
58
+ "build_final_panel",
59
+ "build_transcript_snapshot",
60
+ "build_transcript_view",
61
+ "extract_query_from_meta",
62
+ "format_final_panel_title",
63
+ "render_final_panel",
64
+ ]
@@ -6,6 +6,7 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+
9
10
  from rich.align import Align
10
11
  from rich.markdown import Markdown
11
12
  from rich.spinner import Spinner
@@ -145,3 +146,11 @@ def create_final_panel(content: str, title: str = "Final Result", theme: str = "
145
146
  border_style=SUCCESS,
146
147
  padding=(0, 1),
147
148
  )
149
+
150
+
151
+ __all__ = [
152
+ "create_main_panel",
153
+ "create_tool_panel",
154
+ "create_context_panel",
155
+ "create_final_panel",
156
+ ]
@@ -7,8 +7,22 @@ Authors:
7
7
  from __future__ import annotations
8
8
 
9
9
  from time import monotonic
10
+ from typing import Any
10
11
 
11
- from glaip_sdk.utils.rendering.formatting import get_spinner_char
12
+ from rich.console import Console as RichConsole
13
+ from rich.console import Group
14
+ from rich.measure import Measurement
15
+ from rich.spinner import Spinner
16
+ from rich.text import Text
17
+
18
+ from glaip_sdk.utils.rendering.steps.manager import StepManager
19
+
20
+ _SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
21
+
22
+
23
+ def _spinner_time() -> float:
24
+ """Return the monotonic time used for spinner animation."""
25
+ return monotonic()
12
26
 
13
27
 
14
28
  def get_spinner() -> str:
@@ -16,6 +30,33 @@ def get_spinner() -> str:
16
30
  return get_spinner_char()
17
31
 
18
32
 
33
+ def get_spinner_char() -> str:
34
+ """Return the spinner frame based on elapsed time."""
35
+ frame_index = int(_spinner_time() * 10) % len(_SPINNER_FRAMES)
36
+ return _SPINNER_FRAMES[frame_index]
37
+
38
+
39
+ class TrailingSpinnerLine:
40
+ """Render a text line with a trailing animated Rich spinner."""
41
+
42
+ def __init__(self, base_text: Text, spinner: Spinner) -> None:
43
+ """Initialize spinner line with base text and spinner component."""
44
+ self._base_text = base_text
45
+ self._spinner = spinner
46
+
47
+ def __rich_console__(self, console: RichConsole, options: Any) -> Any: # type: ignore[override]
48
+ """Render the text with trailing animated spinner."""
49
+ spinner_render = self._spinner.render(console.get_time())
50
+ combined = Text.assemble(self._base_text.copy(), " ", spinner_render)
51
+ yield combined
52
+
53
+ def __rich_measure__(self, console: RichConsole, options: Any) -> Measurement: # type: ignore[override]
54
+ """Measure the combined text and spinner dimensions."""
55
+ snapshot = self._spinner.render(0)
56
+ combined = Text.assemble(self._base_text.copy(), " ", snapshot)
57
+ return Measurement.get(console, options, combined)
58
+
59
+
19
60
  def _resolve_elapsed_time(
20
61
  started_at: float | None,
21
62
  server_elapsed_time: float | None,
@@ -131,3 +172,31 @@ def format_tool_title(tool_name: str) -> str:
131
172
 
132
173
  # Convert snake_case to Title Case
133
174
  return clean_name.replace("_", " ").title()
175
+
176
+
177
+ def _has_running_steps(steps: StepManager) -> bool:
178
+ for step in steps.by_id.values():
179
+ if getattr(step, "status", None) not in {"finished", "failed", "stopped"}:
180
+ return True
181
+ return False
182
+
183
+
184
+ def build_progress_footer(
185
+ *,
186
+ state: Any,
187
+ steps: StepManager,
188
+ started_at: float | None,
189
+ server_elapsed_time: float | None,
190
+ ) -> Group | None:
191
+ """Return a trailing progress indicator when work is ongoing."""
192
+ if not _has_running_steps(steps):
193
+ return None
194
+
195
+ indicator = format_working_indicator(
196
+ started_at,
197
+ server_elapsed_time,
198
+ getattr(state, "streaming_started_at", None),
199
+ )
200
+ text = Text(indicator, style="dim")
201
+ spinner = Spinner("dots", style="dim")
202
+ return Group(TrailingSpinnerLine(text, spinner))
@@ -0,0 +1,74 @@
1
+ """Summary panel helpers shared between renderer and viewer.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ from glaip_sdk.utils.rendering.layout.transcript import (
13
+ DEFAULT_TRANSCRIPT_THEME,
14
+ build_transcript_snapshot,
15
+ build_transcript_view,
16
+ normalise_meta_payload,
17
+ )
18
+ from glaip_sdk.utils.rendering.state import RendererState
19
+ from glaip_sdk.utils.rendering.steps import StepManager
20
+
21
+
22
+ def render_summary_panels(
23
+ state: RendererState,
24
+ steps: StepManager,
25
+ *,
26
+ theme: str | None = None,
27
+ summary_window: int | None = None,
28
+ include_query_panel: bool = True,
29
+ include_final_panel: bool = True,
30
+ step_status_overrides: dict[str, str] | None = None,
31
+ ) -> list[Any]:
32
+ """Return shared summary panels for renderer and offline viewer."""
33
+ resolved_theme = theme or DEFAULT_TRANSCRIPT_THEME
34
+ snapshot_source = state.to_snapshot() if hasattr(state, "to_snapshot") else state
35
+ if isinstance(snapshot_source, Mapping):
36
+ raw_meta = snapshot_source.get("meta")
37
+ else:
38
+ raw_meta = getattr(state, "meta", None)
39
+ snapshot_meta = normalise_meta_payload(raw_meta)
40
+ snapshot = build_transcript_snapshot(
41
+ snapshot_source,
42
+ steps,
43
+ meta=snapshot_meta,
44
+ summary_window=summary_window,
45
+ theme=resolved_theme,
46
+ step_status_overrides=step_status_overrides,
47
+ )
48
+ _header, body = build_transcript_view(snapshot, theme=resolved_theme)
49
+
50
+ return [
51
+ renderable
52
+ for renderable in body
53
+ if _should_include_summary_panel(
54
+ renderable,
55
+ include_query_panel=include_query_panel,
56
+ include_final_panel=include_final_panel,
57
+ )
58
+ ]
59
+
60
+
61
+ def _should_include_summary_panel(
62
+ renderable: Any,
63
+ *,
64
+ include_query_panel: bool,
65
+ include_final_panel: bool,
66
+ ) -> bool:
67
+ """Return True when the panel should be included in the summary list."""
68
+ title = getattr(renderable, "title", "")
69
+ normalised = title.lower() if isinstance(title, str) else ""
70
+ if not include_query_panel and normalised == "user request":
71
+ return False
72
+ if not include_final_panel and normalised.startswith("final result"):
73
+ return False
74
+ return True