glaip-sdk 0.6.5b3__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 (145) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +362 -39
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +375 -25
  55. glaip_sdk/cli/slash/tui/__init__.py +28 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +5 -3
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +57 -26
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +89 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/registry/tool.py +273 -66
  109. glaip_sdk/runner/__init__.py +76 -0
  110. glaip_sdk/runner/base.py +84 -0
  111. glaip_sdk/runner/deps.py +115 -0
  112. glaip_sdk/runner/langgraph.py +1055 -0
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  115. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  116. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  117. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  118. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  119. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  120. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  121. glaip_sdk/schedules/__init__.py +22 -0
  122. glaip_sdk/schedules/base.py +291 -0
  123. glaip_sdk/tools/base.py +67 -14
  124. glaip_sdk/utils/__init__.py +1 -0
  125. glaip_sdk/utils/a2a/__init__.py +34 -0
  126. glaip_sdk/utils/a2a/event_processor.py +188 -0
  127. glaip_sdk/utils/agent_config.py +8 -2
  128. glaip_sdk/utils/bundler.py +138 -2
  129. glaip_sdk/utils/import_resolver.py +43 -11
  130. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  131. glaip_sdk/utils/runtime_config.py +120 -0
  132. glaip_sdk/utils/sync.py +31 -11
  133. glaip_sdk/utils/tool_detection.py +301 -0
  134. glaip_sdk/utils/tool_storage_provider.py +140 -0
  135. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
  136. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  137. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  138. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  139. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  140. glaip_sdk/cli/commands/agents.py +0 -1509
  141. glaip_sdk/cli/commands/mcps.py +0 -1356
  142. glaip_sdk/cli/commands/tools.py +0 -576
  143. glaip_sdk/cli/utils.py +0 -263
  144. glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
  145. glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
@@ -6,10 +6,13 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import asyncio
9
10
  import importlib
10
11
  import os
11
12
  import shlex
12
13
  import sys
14
+ import threading
15
+ import time
13
16
  from collections.abc import Callable, Iterable
14
17
  from dataclasses import dataclass
15
18
  from difflib import get_close_matches
@@ -18,6 +21,7 @@ from typing import Any
18
21
 
19
22
  import click
20
23
  from rich.console import Console, Group
24
+ from rich.live import Live
21
25
  from rich.text import Text
22
26
 
23
27
  from glaip_sdk.branding import (
@@ -32,16 +36,20 @@ from glaip_sdk.branding import (
32
36
  SUCCESS_STYLE,
33
37
  WARNING_STYLE,
34
38
  AIPBranding,
39
+ LogoAnimator,
35
40
  )
36
- from glaip_sdk.cli.auth import resolve_api_url_from_context
37
41
  from glaip_sdk.cli.account_store import get_account_store
42
+ from glaip_sdk.cli.auth import resolve_api_url_from_context
38
43
  from glaip_sdk.cli.commands import transcripts as transcripts_cmd
39
44
  from glaip_sdk.cli.commands.configure import _configure_interactive, load_config
40
45
  from glaip_sdk.cli.commands.update import update_command
41
- from glaip_sdk.cli.hints import format_command_hint
46
+ from glaip_sdk.cli.core.context import get_client, restore_slash_session_context
47
+ from glaip_sdk.cli.core.output import format_size
48
+ from glaip_sdk.cli.core.prompting import _fuzzy_pick_for_resources
49
+ from glaip_sdk.cli.hints import command_hint, format_command_hint
42
50
  from glaip_sdk.cli.slash.accounts_controller import AccountsController
43
- from glaip_sdk.cli.slash.agent_session import AgentRunSession
44
51
  from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
52
+ from glaip_sdk.cli.slash.agent_session import AgentRunSession
45
53
  from glaip_sdk.cli.slash.prompt import (
46
54
  FormattedText,
47
55
  PromptSession,
@@ -51,19 +59,13 @@ from glaip_sdk.cli.slash.prompt import (
51
59
  to_formatted_text,
52
60
  )
53
61
  from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
62
+ from glaip_sdk.cli.slash.tui.context import TUIContext
54
63
  from glaip_sdk.cli.transcript import (
55
64
  export_cached_transcript,
56
65
  load_history_snapshot,
57
66
  )
58
67
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
59
68
  from glaip_sdk.cli.update_notifier import maybe_notify_update
60
- from glaip_sdk.cli.utils import (
61
- _fuzzy_pick_for_resources,
62
- command_hint,
63
- format_size,
64
- get_client,
65
- restore_slash_session_context,
66
- )
67
69
  from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
68
70
 
69
71
  SlashHandler = Callable[["SlashSession", list[str], bool], bool]
@@ -145,6 +147,22 @@ def _quick_action_scope(action: dict[str, Any]) -> str:
145
147
  return "global"
146
148
 
147
149
 
150
+ @dataclass
151
+ class AnimationState:
152
+ """State for logo animation shared between threads.
153
+
154
+ Uses mutable lists for integer values to allow thread-safe updates
155
+ without requiring locks or atomic operations.
156
+ """
157
+
158
+ pulse_step: list[int] # Current animation step position
159
+ pulse_direction: list[int] # Direction of pulse (1 or -1)
160
+ step_size: list[int] # Step size for animation
161
+ current_status: list[str] # Current status message
162
+ animation_running: threading.Event # Event signaling animation is running
163
+ stop_requested: threading.Event # Event signaling stop was requested
164
+
165
+
148
166
  class SlashSession:
149
167
  """Interactive command palette controller."""
150
168
 
@@ -156,7 +174,11 @@ class SlashSession:
156
174
  console: Optional console instance, creates default if None
157
175
  """
158
176
  self.ctx = ctx
159
- self.console = console or Console()
177
+ self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
178
+ if console is None:
179
+ self.console = AIPBranding._make_console(force_terminal=self._interactive, soft_wrap=False)
180
+ else:
181
+ self.console = console
160
182
  self._commands: dict[str, SlashCommand] = {}
161
183
  self._unique_commands: dict[str, SlashCommand] = {}
162
184
  self._contextual_commands: dict[str, str] = {}
@@ -165,7 +187,6 @@ class SlashSession:
165
187
  self.recent_agents: list[dict[str, str]] = []
166
188
  self.last_run_input: str | None = None
167
189
  self._should_exit = False
168
- self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
169
190
  self._config_cache: dict[str, Any] | None = None
170
191
  self._welcome_rendered = False
171
192
  self._active_renderer: Any | None = None
@@ -189,6 +210,16 @@ class SlashSession:
189
210
  self._update_notifier = maybe_notify_update
190
211
  self._home_hint_shown = False
191
212
  self._agent_transcript_ready: dict[str, str] = {}
213
+ self.tui_ctx: TUIContext | None = None
214
+
215
+ # Animation configuration constants
216
+ ANIMATION_FPS = 20
217
+ ANIMATION_FRAME_DURATION = 1.0 / ANIMATION_FPS # 0.05 seconds
218
+ ANIMATION_STARTUP_DELAY = 0.1 # Delay to ensure animation starts
219
+
220
+ # Startup UI constants
221
+ INITIALIZING_STATUS = "Initializing..."
222
+ CLI_HEADING_MARKUP = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
192
223
 
193
224
  # ------------------------------------------------------------------
194
225
  # Session orchestration
@@ -229,11 +260,18 @@ class SlashSession:
229
260
  self._run_non_interactive(initial_commands)
230
261
  return
231
262
 
232
- if not self._ensure_configuration():
263
+ # Use animated logo during initialization if supported
264
+ animator = LogoAnimator(console=self.console)
265
+ if animator.should_animate() and self._interactive:
266
+ config_available = self._run_with_animated_logo(animator)
267
+ else:
268
+ # Fallback to static logo for non-TTY or NO_COLOR
269
+ config_available = self._run_with_static_logo(animator)
270
+
271
+ if not config_available:
233
272
  return
234
273
 
235
- self._maybe_show_update_prompt()
236
- self._render_header(initial=not self._welcome_rendered)
274
+ self._render_header(initial=not self._welcome_rendered, show_branding=False)
237
275
  if not self._default_actions_shown:
238
276
  self._show_default_quick_actions()
239
277
  self._run_interactive_loop()
@@ -241,6 +279,283 @@ class SlashSession:
241
279
  if ctx_obj is not None:
242
280
  restore_slash_session_context(ctx_obj, previous_session)
243
281
 
282
+ def _initialize_tui_context(self) -> None:
283
+ """Initialize TUI context with error handling.
284
+
285
+ Sets self.tui_ctx to None if initialization fails.
286
+ """
287
+ try:
288
+ self.tui_ctx = asyncio.run(TUIContext.create(detect_osc11=False))
289
+ except RuntimeError:
290
+ try:
291
+ loop = asyncio.get_event_loop()
292
+ except RuntimeError:
293
+ self.tui_ctx = None
294
+ else:
295
+ if loop.is_running():
296
+ self.tui_ctx = None
297
+ else:
298
+ self.tui_ctx = loop.run_until_complete(TUIContext.create(detect_osc11=False))
299
+ except Exception:
300
+ self.tui_ctx = None
301
+
302
+ def _run_initialization_tasks(
303
+ self,
304
+ current_status: list[str],
305
+ animation_running: threading.Event,
306
+ status_callback: Callable[[str], None] | None = None,
307
+ ) -> bool:
308
+ """Run initialization tasks with status updates.
309
+
310
+ Args:
311
+ current_status: Mutable list with current status message.
312
+ animation_running: Event to signal animation state.
313
+ status_callback: Optional callback to invoke when status changes.
314
+
315
+ Returns:
316
+ True if configuration is available, False otherwise.
317
+ """
318
+ # Task 1: TUI Context.
319
+ current_status[0] = "Detecting terminal..."
320
+ if status_callback:
321
+ status_callback(current_status[0])
322
+ self._initialize_tui_context()
323
+
324
+ # Task 2: Configuration.
325
+ current_status[0] = "Connecting to API..."
326
+ if status_callback:
327
+ status_callback(current_status[0])
328
+ if not self._ensure_configuration():
329
+ animation_running.clear()
330
+ return False
331
+
332
+ # Task 3: Updates.
333
+ current_status[0] = "Checking for updates..."
334
+ if status_callback:
335
+ status_callback(current_status[0])
336
+ # Defer update prompt if we are in animated initialization to avoid blocking/cluttering
337
+ self._maybe_show_update_prompt(defer=bool(status_callback is None))
338
+ return True
339
+
340
+ def _update_pulse_step(
341
+ self,
342
+ state: AnimationState,
343
+ animator: LogoAnimator,
344
+ ) -> bool:
345
+ """Update pulse step and direction.
346
+
347
+ Args:
348
+ state: Animation state container.
349
+ animator: LogoAnimator instance for animation.
350
+
351
+ Returns:
352
+ True if animation should continue, False if should stop.
353
+ """
354
+ state.pulse_step[0] += state.pulse_direction[0] * state.step_size[0]
355
+ if state.pulse_step[0] >= animator.max_width + 5:
356
+ state.pulse_step[0] = animator.max_width + 5
357
+ state.pulse_direction[0] = -1
358
+ return not state.stop_requested.is_set()
359
+ if state.pulse_step[0] <= -5:
360
+ state.pulse_step[0] = -5
361
+ state.pulse_direction[0] = 1
362
+ return not state.stop_requested.is_set()
363
+ return True
364
+
365
+ def _create_animation_updater(
366
+ self,
367
+ animator: LogoAnimator,
368
+ state: AnimationState,
369
+ heading: Text,
370
+ ) -> Callable[[Live], None]:
371
+ """Create animation update function for background thread.
372
+
373
+ Args:
374
+ animator: LogoAnimator instance for animation.
375
+ state: Animation state container.
376
+ heading: Text heading for frames.
377
+
378
+ Returns:
379
+ Function to update animation in background thread.
380
+ """
381
+
382
+ def build_frame(step: int, status_text: str) -> Group:
383
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
384
+
385
+ def update_animation(live: Live) -> None:
386
+ """Update animation in background thread."""
387
+ while state.animation_running.is_set():
388
+ # Calculate next step
389
+ if not self._update_pulse_step(state, animator):
390
+ break
391
+
392
+ # Update frame with current status
393
+ try:
394
+ live.update(build_frame(state.pulse_step[0], state.current_status[0]))
395
+ except Exception:
396
+ # Animation may be stopped, ignore errors
397
+ break
398
+ time.sleep(self.ANIMATION_FRAME_DURATION)
399
+ state.animation_running.clear()
400
+
401
+ return update_animation
402
+
403
+ def _stop_animation_thread(
404
+ self,
405
+ animation_thread: threading.Thread,
406
+ state: AnimationState,
407
+ ) -> None:
408
+ """Stop animation thread gracefully.
409
+
410
+ Args:
411
+ animation_thread: Thread running animation.
412
+ state: Animation state container.
413
+ """
414
+ state.stop_requested.set()
415
+ state.step_size[0] = 3
416
+ animation_thread.join(timeout=1.5)
417
+ if animation_thread.is_alive():
418
+ state.animation_running.clear()
419
+ animation_thread.join(timeout=0.2)
420
+
421
+ def _run_animated_initialization(
422
+ self,
423
+ live: Live,
424
+ animator: LogoAnimator,
425
+ state: AnimationState,
426
+ heading: Text,
427
+ banner: Text,
428
+ ) -> bool:
429
+ """Run initialization tasks with animated logo.
430
+
431
+ Args:
432
+ live: Live context for animation updates.
433
+ animator: LogoAnimator instance for animation.
434
+ state: Animation state container.
435
+ heading: Text heading for frames.
436
+ banner: Text banner for final display.
437
+
438
+ Returns:
439
+ True if configuration is available, False otherwise.
440
+ """
441
+
442
+ def build_banner() -> Group:
443
+ return Group(heading, Text(""), banner)
444
+
445
+ update_animation = self._create_animation_updater(
446
+ animator,
447
+ state,
448
+ heading,
449
+ )
450
+
451
+ # Start animation thread.
452
+ animation_thread = threading.Thread(target=update_animation, args=(live,), daemon=True)
453
+ animation_thread.start()
454
+
455
+ # Small delay to ensure animation starts.
456
+ time.sleep(self.ANIMATION_STARTUP_DELAY)
457
+
458
+ # Run initialization tasks.
459
+ if not self._run_initialization_tasks(state.current_status, state.animation_running, status_callback=None):
460
+ return False
461
+
462
+ # Stop animation and show final banner.
463
+ self._stop_animation_thread(animation_thread, state)
464
+ live.update(build_banner())
465
+ return True
466
+
467
+ def _run_with_animated_logo(self, animator: LogoAnimator) -> bool:
468
+ """Run initialization with animated logo.
469
+
470
+ Args:
471
+ animator: LogoAnimator instance for animation.
472
+
473
+ Returns:
474
+ True if configuration is available, False otherwise.
475
+ """
476
+ state = AnimationState(
477
+ pulse_step=[0], # Use list for mutable shared state.
478
+ pulse_direction=[1], # Use list for mutable shared state.
479
+ step_size=[1],
480
+ current_status=[self.INITIALIZING_STATUS],
481
+ animation_running=threading.Event(),
482
+ stop_requested=threading.Event(),
483
+ )
484
+ state.animation_running.set()
485
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
486
+ banner = Text.from_markup(self._branding.get_welcome_banner())
487
+
488
+ def build_frame(step: int, status_text: str) -> Group:
489
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
490
+
491
+ try:
492
+ with Live(
493
+ build_frame(0, state.current_status[0]),
494
+ console=self.console,
495
+ refresh_per_second=self.ANIMATION_FPS,
496
+ transient=False,
497
+ ) as live:
498
+ return self._run_animated_initialization(
499
+ live,
500
+ animator,
501
+ state,
502
+ heading,
503
+ banner,
504
+ )
505
+ except KeyboardInterrupt:
506
+ # Graceful exit on Ctrl+C
507
+ state.animation_running.clear()
508
+ # Align with static path: show heading and cancellation message
509
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
510
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
511
+ return False
512
+
513
+ def _run_with_static_logo(self, animator: LogoAnimator) -> bool:
514
+ """Run initialization with static logo (non-TTY or NO_COLOR).
515
+
516
+ Args:
517
+ animator: LogoAnimator instance for static display.
518
+
519
+ Returns:
520
+ True if configuration is available, False otherwise.
521
+ """
522
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
523
+ banner = Text.from_markup(self._branding.get_welcome_banner())
524
+
525
+ def build_frame(status_text: str) -> Group:
526
+ return Group(heading, Text(""), animator.static_frame(status_text))
527
+
528
+ def build_banner() -> Group:
529
+ return Group(heading, Text(""), banner)
530
+
531
+ try:
532
+ with Live(
533
+ build_frame(self.INITIALIZING_STATUS),
534
+ console=self.console,
535
+ refresh_per_second=4,
536
+ transient=False,
537
+ ) as live:
538
+ # Run initialization tasks with status updates, reusing shared logic.
539
+ current_status = [self.INITIALIZING_STATUS]
540
+ animation_running = threading.Event()
541
+ animation_running.set()
542
+
543
+ # Update Live display when status changes via callback.
544
+ def update_display(status: str) -> None:
545
+ """Update Live display with current status."""
546
+ live.update(build_frame(status))
547
+
548
+ if not self._run_initialization_tasks(
549
+ current_status, animation_running, status_callback=update_display
550
+ ):
551
+ return False
552
+
553
+ live.update(build_banner())
554
+ return True
555
+ except KeyboardInterrupt:
556
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
557
+ return False
558
+
244
559
  def _run_interactive_loop(self) -> None:
245
560
  """Run the main interactive command loop."""
246
561
  while not self._should_exit:
@@ -548,7 +863,7 @@ class SlashSession:
548
863
  try:
549
864
  # Use the modern account-aware wizard directly (bypasses legacy config gating)
550
865
  _configure_interactive(account_name=None)
551
- self._config_cache = None
866
+ self.on_account_switched()
552
867
  if self._suppress_login_layout:
553
868
  self._welcome_rendered = False
554
869
  self._default_actions_shown = False
@@ -1211,6 +1526,33 @@ class SlashSession:
1211
1526
  self._client = get_client(self.ctx)
1212
1527
  return self._client
1213
1528
 
1529
+ def on_account_switched(self, _account_name: str | None = None) -> None:
1530
+ """Reset any state that depends on the active account.
1531
+
1532
+ The active account can change via `/accounts` (or other flows that call
1533
+ AccountStore.set_active_account). The slash session caches a configured
1534
+ client instance, so we must invalidate it to avoid leaking the previous
1535
+ account's API URL/key into subsequent commands like `/agents` or `/runs`.
1536
+
1537
+ This method clears:
1538
+ - Client and config cache (account-specific credentials)
1539
+ - Current agent and recent agents (agent data is account-scoped)
1540
+ - Runs pagination state (runs are account-scoped)
1541
+ - Active renderer and transcript ready state (UI state tied to account context)
1542
+ - Contextual commands (may be account-specific)
1543
+
1544
+ These broader resets ensure a clean slate when switching accounts, preventing
1545
+ stale data from the previous account from appearing in the new account's context.
1546
+ """
1547
+ self._client = None
1548
+ self._config_cache = None
1549
+ self._current_agent = None
1550
+ self.recent_agents = []
1551
+ self._runs_pagination_state.clear()
1552
+ self.clear_active_renderer()
1553
+ self.clear_agent_transcript_ready()
1554
+ self.set_contextual_commands(None)
1555
+
1214
1556
  def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
1215
1557
  """Set context-specific commands that should appear in completions."""
1216
1558
  self._contextual_commands = dict(commands or {})
@@ -1246,6 +1588,7 @@ class SlashSession:
1246
1588
  *,
1247
1589
  focus_agent: bool = False,
1248
1590
  initial: bool = False,
1591
+ show_branding: bool = True,
1249
1592
  ) -> None:
1250
1593
  """Render the session header with branding and status.
1251
1594
 
@@ -1253,14 +1596,16 @@ class SlashSession:
1253
1596
  active_agent: Optional active agent to display.
1254
1597
  focus_agent: Whether to focus on agent display.
1255
1598
  initial: Whether this is the initial render.
1599
+ show_branding: Whether to render the branding banner.
1256
1600
  """
1257
1601
  if focus_agent and active_agent is not None:
1258
1602
  self._render_focused_agent_header(active_agent)
1259
1603
  return
1260
1604
 
1261
1605
  full_header = initial or not self._welcome_rendered
1262
- if full_header:
1606
+ if full_header and show_branding:
1263
1607
  self._render_branding_banner()
1608
+ if full_header:
1264
1609
  self.console.rule(style=PRIMARY)
1265
1610
  self._render_main_header(active_agent, full=full_header)
1266
1611
  if full_header:
@@ -1270,14 +1615,17 @@ class SlashSession:
1270
1615
  def _render_branding_banner(self) -> None:
1271
1616
  """Render the GL AIP branding banner."""
1272
1617
  banner = self._branding.get_welcome_banner()
1273
- heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
1618
+ heading = self.CLI_HEADING_MARKUP
1274
1619
  self.console.print(heading)
1275
1620
  self.console.print()
1276
1621
  self.console.print(banner)
1277
1622
 
1278
- def _maybe_show_update_prompt(self) -> None:
1623
+ def _maybe_show_update_prompt(self, *, defer: bool = False) -> None:
1279
1624
  """Display update prompt once per session when applicable."""
1280
- if self._update_prompt_shown:
1625
+ if self._update_prompt_shown or (defer and not self._update_prompt_shown):
1626
+ if defer:
1627
+ # Just mark as ready to show, but don't show yet
1628
+ return
1281
1629
  return
1282
1630
 
1283
1631
  self._update_notifier(
@@ -1340,14 +1688,16 @@ class SlashSession:
1340
1688
  f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
1341
1689
  )
1342
1690
  status_line = f"[{SUCCESS_STYLE}]ready[/]"
1343
- status_line += " · transcript ready" if transcript_status["transcript_ready"] else " · transcript pending"
1691
+ if not transcript_status["has_transcript"]:
1692
+ status_line += " · no transcript"
1693
+ elif transcript_status["transcript_ready"]:
1694
+ status_line += " · transcript ready"
1695
+ else:
1696
+ status_line += " · transcript pending"
1344
1697
  header_grid.add_row(primary_line, status_line)
1345
1698
 
1346
1699
  if agent_info["description"]:
1347
- description = agent_info["description"]
1348
- if not transcript_status["transcript_ready"]:
1349
- description = f"{description} (transcript pending)"
1350
- header_grid.add_row(f"[dim]{description}[/dim]", "")
1700
+ header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
1351
1701
 
1352
1702
  return header_grid
1353
1703
 
@@ -1,9 +1,36 @@
1
1
  """Textual UI helpers for slash commands."""
2
2
 
3
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
4
+ from glaip_sdk.cli.slash.tui.context import TUIContext
5
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
6
+ from glaip_sdk.cli.slash.tui.keybind_registry import (
7
+ Keybind,
8
+ KeybindRegistry,
9
+ format_key_sequence,
10
+ parse_key_sequence,
11
+ )
3
12
  from glaip_sdk.cli.slash.tui.remote_runs_app import (
4
13
  RemoteRunsTextualApp,
5
14
  RemoteRunsTUICallbacks,
6
15
  run_remote_runs_textual,
7
16
  )
17
+ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_terminal_background
18
+ from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
8
19
 
9
- __all__ = ["RemoteRunsTextualApp", "RemoteRunsTUICallbacks", "run_remote_runs_textual"]
20
+ __all__ = [
21
+ "TUIContext",
22
+ "ToastBus",
23
+ "ToastVariant",
24
+ "TerminalCapabilities",
25
+ "detect_terminal_background",
26
+ "RemoteRunsTextualApp",
27
+ "RemoteRunsTUICallbacks",
28
+ "run_remote_runs_textual",
29
+ "KeybindRegistry",
30
+ "Keybind",
31
+ "parse_key_sequence",
32
+ "format_key_sequence",
33
+ "ClipboardAdapter",
34
+ "ClipboardResult",
35
+ "PulseIndicator",
36
+ ]