glaip-sdk 0.3.0__py3-none-any.whl → 0.5.0__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 (58) hide show
  1. glaip_sdk/cli/account_store.py +522 -0
  2. glaip_sdk/cli/auth.py +224 -8
  3. glaip_sdk/cli/commands/accounts.py +414 -0
  4. glaip_sdk/cli/commands/agents.py +2 -2
  5. glaip_sdk/cli/commands/common_config.py +65 -0
  6. glaip_sdk/cli/commands/configure.py +153 -87
  7. glaip_sdk/cli/commands/mcps.py +191 -44
  8. glaip_sdk/cli/commands/transcripts.py +1 -1
  9. glaip_sdk/cli/config.py +31 -3
  10. glaip_sdk/cli/display.py +1 -1
  11. glaip_sdk/cli/hints.py +57 -0
  12. glaip_sdk/cli/io.py +6 -3
  13. glaip_sdk/cli/main.py +181 -79
  14. glaip_sdk/cli/masking.py +14 -1
  15. glaip_sdk/cli/slash/agent_session.py +2 -1
  16. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  17. glaip_sdk/cli/slash/session.py +11 -9
  18. glaip_sdk/cli/slash/tui/remote_runs_app.py +2 -3
  19. glaip_sdk/cli/transcript/capture.py +12 -18
  20. glaip_sdk/cli/transcript/viewer.py +13 -646
  21. glaip_sdk/cli/update_notifier.py +2 -1
  22. glaip_sdk/cli/utils.py +95 -139
  23. glaip_sdk/client/agents.py +2 -4
  24. glaip_sdk/client/main.py +2 -18
  25. glaip_sdk/client/mcps.py +11 -1
  26. glaip_sdk/client/run_rendering.py +90 -111
  27. glaip_sdk/client/shared.py +21 -0
  28. glaip_sdk/models.py +8 -7
  29. glaip_sdk/utils/display.py +23 -15
  30. glaip_sdk/utils/rendering/__init__.py +6 -13
  31. glaip_sdk/utils/rendering/formatting.py +5 -30
  32. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  33. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  34. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  35. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  36. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  37. glaip_sdk/utils/rendering/models.py +1 -0
  38. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  39. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  40. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  41. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  42. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  43. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  44. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  45. glaip_sdk/utils/rendering/state.py +204 -0
  46. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  47. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  48. glaip_sdk/utils/rendering/steps/format.py +176 -0
  49. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  50. glaip_sdk/utils/rendering/timing.py +36 -0
  51. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  52. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  53. glaip_sdk/utils/validation.py +13 -21
  54. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/METADATA +1 -1
  55. glaip_sdk-0.5.0.dist-info/RECORD +113 -0
  56. glaip_sdk-0.3.0.dist-info/RECORD +0 -94
  57. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/WHEEL +0 -0
  58. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -7,9 +7,9 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import io
11
10
  import json
12
11
  import logging
12
+ from collections.abc import Callable
13
13
  from time import monotonic
14
14
  from typing import Any
15
15
 
@@ -19,8 +19,17 @@ from rich.console import Console as _Console
19
19
  from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
20
20
  from glaip_sdk.utils.client_utils import iter_sse_events
21
21
  from glaip_sdk.utils.rendering.models import RunStats
22
- from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
23
- from glaip_sdk.utils.rendering.renderer.config import RendererConfig
22
+ from glaip_sdk.utils.rendering.renderer import (
23
+ RendererFactoryOptions,
24
+ RichStreamRenderer,
25
+ make_default_renderer,
26
+ make_minimal_renderer,
27
+ make_silent_renderer,
28
+ make_verbose_renderer,
29
+ )
30
+ from glaip_sdk.utils.rendering.state import TranscriptBuffer
31
+
32
+ NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
24
33
 
25
34
 
26
35
  def _coerce_to_string(value: Any) -> str:
@@ -36,41 +45,6 @@ def _has_visible_text(value: Any) -> bool:
36
45
  return isinstance(value, str) and bool(value.strip())
37
46
 
38
47
 
39
- def _update_state_transcript(state: Any, text_value: str) -> bool:
40
- """Inject transcript text into renderer state if possible."""
41
- if state is None:
42
- return False
43
-
44
- updated = False
45
-
46
- if hasattr(state, "final_text") and not _has_visible_text(getattr(state, "final_text", "")):
47
- try:
48
- state.final_text = text_value
49
- updated = True
50
- except Exception:
51
- pass
52
-
53
- buffer = getattr(state, "buffer", None)
54
- if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
55
- buffer.append(text_value)
56
- updated = True
57
-
58
- return updated
59
-
60
-
61
- def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
62
- """Populate the renderer (or its state) with the supplied text."""
63
- state = getattr(renderer, "state", None)
64
- if _update_state_transcript(state, text_value):
65
- return
66
-
67
- if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
68
- try:
69
- renderer.final_text = text_value
70
- except Exception:
71
- pass
72
-
73
-
74
48
  class AgentRunRenderingManager:
75
49
  """Coordinate renderer creation and streaming event handling."""
76
50
 
@@ -81,6 +55,7 @@ class AgentRunRenderingManager:
81
55
  logger: Optional logger instance, creates default if None
82
56
  """
83
57
  self._logger = logger or logging.getLogger(__name__)
58
+ self._buffer_factory = TranscriptBuffer
84
59
 
85
60
  # --------------------------------------------------------------------- #
86
61
  # Renderer setup helpers
@@ -92,17 +67,38 @@ class AgentRunRenderingManager:
92
67
  verbose: bool = False,
93
68
  ) -> RichStreamRenderer:
94
69
  """Create an appropriate renderer based on the supplied spec."""
70
+ transcript_buffer = self._buffer_factory()
71
+ base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
95
72
  if isinstance(renderer_spec, RichStreamRenderer):
96
73
  return renderer_spec
97
74
 
98
75
  if isinstance(renderer_spec, str):
99
- if renderer_spec == "silent":
100
- return self._create_silent_renderer()
101
- if renderer_spec == "minimal":
102
- return self._create_minimal_renderer()
103
- return self._create_default_renderer(verbose)
76
+ lowered = renderer_spec.lower()
77
+ if lowered == "silent":
78
+ return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
79
+ if lowered == "minimal":
80
+ return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
81
+ if lowered == "verbose":
82
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
83
+
84
+ if verbose:
85
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
104
86
 
105
- return self._create_default_renderer(verbose)
87
+ default_options = RendererFactoryOptions(
88
+ console=_Console(),
89
+ transcript_buffer=transcript_buffer,
90
+ verbose=verbose,
91
+ )
92
+ return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
93
+
94
+ @staticmethod
95
+ def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
96
+ """Attach a captured transcript buffer to a renderer for later inspection."""
97
+ try:
98
+ renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
99
+ except Exception:
100
+ pass
101
+ return renderer
106
102
 
107
103
  def build_initial_metadata(
108
104
  self,
@@ -123,70 +119,6 @@ class AgentRunRenderingManager:
123
119
  """Notify renderer that streaming is starting."""
124
120
  renderer.on_start(meta)
125
121
 
126
- def _create_silent_renderer(self) -> RichStreamRenderer:
127
- """Create a silent renderer that outputs to a string buffer.
128
-
129
- Returns:
130
- RichStreamRenderer configured for silent output.
131
- """
132
- silent_config = RendererConfig(
133
- live=False,
134
- persist_live=False,
135
- render_thinking=False,
136
- )
137
- return RichStreamRenderer(
138
- console=_Console(file=io.StringIO(), force_terminal=False),
139
- cfg=silent_config,
140
- verbose=False,
141
- )
142
-
143
- def _create_minimal_renderer(self) -> RichStreamRenderer:
144
- """Create a minimal renderer with reduced output.
145
-
146
- Returns:
147
- RichStreamRenderer configured for minimal output.
148
- """
149
- minimal_config = RendererConfig(
150
- live=False,
151
- persist_live=False,
152
- render_thinking=False,
153
- )
154
- return RichStreamRenderer(
155
- console=_Console(),
156
- cfg=minimal_config,
157
- verbose=False,
158
- )
159
-
160
- def _create_verbose_renderer(self) -> RichStreamRenderer:
161
- """Create a verbose renderer with detailed output.
162
-
163
- Returns:
164
- RichStreamRenderer configured for verbose output.
165
- """
166
- verbose_config = RendererConfig(
167
- live=False,
168
- append_finished_snapshots=False,
169
- )
170
- return RichStreamRenderer(
171
- console=_Console(),
172
- cfg=verbose_config,
173
- verbose=True,
174
- )
175
-
176
- def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
177
- """Create the default renderer based on verbosity.
178
-
179
- Args:
180
- verbose: Whether to create a verbose renderer.
181
-
182
- Returns:
183
- RichStreamRenderer instance.
184
- """
185
- if verbose:
186
- return self._create_verbose_renderer()
187
- default_config = RendererConfig()
188
- return RichStreamRenderer(console=_Console(), cfg=default_config)
189
-
190
122
  # --------------------------------------------------------------------- #
191
123
  # Streaming event handling
192
124
  # --------------------------------------------------------------------- #
@@ -382,7 +314,52 @@ class AgentRunRenderingManager:
382
314
  return
383
315
 
384
316
  text_value = _coerce_to_string(text)
385
- _update_renderer_transcript(renderer, text_value)
317
+ state = getattr(renderer, "state", None)
318
+ if state is None:
319
+ self._ensure_renderer_text(renderer, text_value)
320
+ return
321
+
322
+ self._ensure_state_final_text(state, text_value)
323
+ self._ensure_state_buffer(state, text_value)
324
+
325
+ def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
326
+ """Best-effort assignment for renderer.final_text."""
327
+ if not hasattr(renderer, "final_text"):
328
+ return
329
+ current_text = getattr(renderer, "final_text", "")
330
+ if _has_visible_text(current_text):
331
+ return
332
+ self._safe_set_attr(renderer, "final_text", text_value)
333
+
334
+ def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
335
+ """Best-effort assignment for renderer.state.final_text."""
336
+ current_text = getattr(state, "final_text", "")
337
+ if _has_visible_text(current_text):
338
+ return
339
+ self._safe_set_attr(state, "final_text", text_value)
340
+
341
+ def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
342
+ """Append fallback text to the state buffer when available."""
343
+ buffer = getattr(state, "buffer", None)
344
+ if not hasattr(buffer, "append"):
345
+ return
346
+ self._safe_append(buffer.append, text_value)
347
+
348
+ @staticmethod
349
+ def _safe_set_attr(target: Any, attr: str, value: str) -> None:
350
+ """Assign attribute while masking renderer-specific failures."""
351
+ try:
352
+ setattr(target, attr, value)
353
+ except Exception:
354
+ pass
355
+
356
+ @staticmethod
357
+ def _safe_append(appender: Callable[[str], Any], value: str) -> None:
358
+ """Invoke append-like functions without leaking renderer errors."""
359
+ try:
360
+ appender(value)
361
+ except Exception:
362
+ pass
386
363
 
387
364
  # --------------------------------------------------------------------- #
388
365
  # Finalisation helpers
@@ -409,7 +386,9 @@ class AgentRunRenderingManager:
409
386
  elif hasattr(renderer, "buffer"):
410
387
  buffer_values = renderer.buffer
411
388
 
412
- if buffer_values is not None:
389
+ if isinstance(buffer_values, TranscriptBuffer):
390
+ rendered_text = buffer_values.render()
391
+ elif buffer_values is not None:
413
392
  try:
414
393
  rendered_text = "".join(buffer_values)
415
394
  except TypeError:
@@ -420,7 +399,7 @@ class AgentRunRenderingManager:
420
399
  self._ensure_renderer_final_content(renderer, fallback_text)
421
400
 
422
401
  renderer.on_complete(st)
423
- return final_text or rendered_text or "No response content received."
402
+ return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
424
403
 
425
404
 
426
405
  def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
@@ -0,0 +1,21 @@
1
+ """Shared helpers for client configuration wiring.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from glaip_sdk.client.base import BaseClient
12
+
13
+
14
+ def build_shared_config(client: BaseClient) -> dict[str, Any]:
15
+ """Return the keyword arguments used to initialize sub-clients."""
16
+ return {
17
+ "parent_client": client,
18
+ "api_url": client.api_url,
19
+ "api_key": client.api_key,
20
+ "timeout": client._timeout,
21
+ }
glaip_sdk/models.py CHANGED
@@ -23,21 +23,22 @@ class Agent(BaseModel):
23
23
  id: str
24
24
  name: str
25
25
  instruction: str | None = None
26
- description: str | None = None # Add missing description field
26
+ description: str | None = None
27
27
  type: str | None = None
28
28
  framework: str | None = None
29
29
  version: str | None = None
30
- tools: list[dict[str, Any]] | None = None # Backend returns ToolReference objects
31
- agents: list[dict[str, Any]] | None = None # Backend returns AgentReference objects
32
- mcps: list[dict[str, Any]] | None = None # Backend returns MCPReference objects
33
- tool_configs: dict[str, Any] | None = None # Backend returns tool configurations keyed by tool UUID
30
+ tools: list[dict[str, Any]] | None = None
31
+ agents: list[dict[str, Any]] | None = None
32
+ mcps: list[dict[str, Any]] | None = None
33
+ tool_configs: dict[str, Any] | None = None
34
+ mcp_configs: dict[str, Any] | None = None
34
35
  agent_config: dict[str, Any] | None = None
35
36
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT
36
37
  metadata: dict[str, Any] | None = None
37
38
  language_model_id: str | None = None
38
39
  a2a_profile: dict[str, Any] | None = None
39
- created_at: datetime | None = None # Backend returns creation timestamp
40
- updated_at: datetime | None = None # Backend returns last update timestamp
40
+ created_at: datetime | None = None
41
+ updated_at: datetime | None = None
41
42
  _client: Any = None
42
43
 
43
44
  def _set_client(self, client: Any) -> "Agent":
@@ -4,6 +4,9 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
7
10
  from typing import TYPE_CHECKING, Any
8
11
 
9
12
  from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE
@@ -13,42 +16,47 @@ if TYPE_CHECKING: # pragma: no cover - import-time typing helpers
13
16
  from rich.console import Console
14
17
  from rich.text import Text
15
18
 
16
- from glaip_sdk.rich_components import AIPPanel
19
+ from glaip_sdk.rich_components import AIPanel
20
+ else: # pragma: no cover - runtime fallback for type checking
21
+ AIPanel = Any # type: ignore[assignment]
17
22
 
18
23
 
19
24
  def _check_rich_available() -> bool:
20
- """Check if Rich and our custom components can be imported."""
25
+ """Return True when core Rich display dependencies are importable."""
21
26
  try:
22
27
  __import__("rich.console")
23
28
  __import__("rich.text")
24
29
  __import__("glaip_sdk.rich_components")
25
- return True
26
30
  except Exception:
27
31
  return False
32
+ return True
28
33
 
29
34
 
30
35
  RICH_AVAILABLE = _check_rich_available()
31
36
 
32
37
 
33
- def _create_console() -> "Console":
38
+ def _create_console() -> Console:
34
39
  """Return a Console instance with lazy import to ease mocking."""
35
- from rich.console import Console # Local import for test friendliness
36
-
37
- return Console()
40
+ if not RICH_AVAILABLE: # pragma: no cover - defensive guard
41
+ raise RuntimeError("Rich Console is not available")
42
+ console_module = import_module("rich.console")
43
+ return console_module.Console()
38
44
 
39
45
 
40
- def _create_text(*args: Any, **kwargs: Any) -> "Text":
46
+ def _create_text(*args: Any, **kwargs: Any) -> Text:
41
47
  """Return a Text instance with lazy import to ease mocking."""
42
- from rich.text import Text # Local import for test friendliness
48
+ if not RICH_AVAILABLE: # pragma: no cover - defensive guard
49
+ raise RuntimeError("Rich Text is not available")
50
+ text_module = import_module("rich.text")
51
+ return text_module.Text(*args, **kwargs)
43
52
 
44
- return Text(*args, **kwargs)
45
53
 
46
-
47
- def _create_panel(*args: Any, **kwargs: Any) -> "AIPPanel":
54
+ def _create_panel(*args: Any, **kwargs: Any) -> AIPanel:
48
55
  """Return an AIPPanel instance with lazy import to ease mocking."""
49
- from glaip_sdk.rich_components import AIPPanel # Local import for test friendliness
50
-
51
- return AIPPanel(*args, **kwargs)
56
+ if not RICH_AVAILABLE: # pragma: no cover - defensive guard
57
+ raise RuntimeError("AIPPanel is not available")
58
+ components = import_module("glaip_sdk.rich_components")
59
+ return components.AIPPanel(*args, **kwargs)
52
60
 
53
61
 
54
62
  def print_agent_output(output: str, title: str = "Agent Output") -> None:
@@ -10,7 +10,7 @@ from typing import Any
10
10
  from rich.console import Console
11
11
 
12
12
  from glaip_sdk.models.agent_runs import RunWithOutput
13
- from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
13
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event, render_debug_event_stream
14
14
 
15
15
 
16
16
  def _parse_event_received_timestamp(event: dict[str, Any]) -> datetime | None:
@@ -80,18 +80,11 @@ def render_remote_sse_transcript(
80
80
  console.print("[bold]SSE Events[/bold]")
81
81
  console.print("[dim]────────────────────────────────────────────────────────[/dim]")
82
82
 
83
- baseline: datetime | None = None
84
- for event in run.output:
85
- received_ts = _parse_event_received_timestamp(event)
86
- if baseline is None and received_ts is not None:
87
- baseline = received_ts
88
- render_debug_event(
89
- event,
90
- console,
91
- received_ts=received_ts,
92
- baseline_ts=baseline,
93
- )
94
-
83
+ render_debug_event_stream(
84
+ run.output,
85
+ console,
86
+ resolve_timestamp=_parse_event_received_timestamp,
87
+ )
95
88
  console.print()
96
89
 
97
90
 
@@ -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))