glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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 (127) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +217 -42
  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 +119 -0
  8. glaip_sdk/cli/commands/agents/_common.py +561 -0
  9. glaip_sdk/cli/commands/agents/create.py +151 -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 +369 -23
  55. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +87 -0
  60. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  61. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  62. glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +374 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +5 -3
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +50 -8
  78. glaip_sdk/client/hitl.py +136 -0
  79. glaip_sdk/client/main.py +7 -1
  80. glaip_sdk/client/mcps.py +44 -13
  81. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  82. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  83. glaip_sdk/client/payloads/agent/responses.py +43 -0
  84. glaip_sdk/client/run_rendering.py +414 -3
  85. glaip_sdk/client/schedules.py +439 -0
  86. glaip_sdk/client/tools.py +57 -26
  87. glaip_sdk/guardrails/__init__.py +80 -0
  88. glaip_sdk/guardrails/serializer.py +89 -0
  89. glaip_sdk/hitl/__init__.py +48 -0
  90. glaip_sdk/hitl/base.py +64 -0
  91. glaip_sdk/hitl/callback.py +43 -0
  92. glaip_sdk/hitl/local.py +121 -0
  93. glaip_sdk/hitl/remote.py +523 -0
  94. glaip_sdk/models/__init__.py +17 -0
  95. glaip_sdk/models/agent_runs.py +2 -1
  96. glaip_sdk/models/schedule.py +224 -0
  97. glaip_sdk/payload_schemas/agent.py +1 -0
  98. glaip_sdk/payload_schemas/guardrails.py +34 -0
  99. glaip_sdk/registry/tool.py +273 -59
  100. glaip_sdk/runner/__init__.py +20 -3
  101. glaip_sdk/runner/deps.py +5 -8
  102. glaip_sdk/runner/langgraph.py +318 -42
  103. glaip_sdk/runner/logging_config.py +77 -0
  104. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  105. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  106. glaip_sdk/schedules/__init__.py +22 -0
  107. glaip_sdk/schedules/base.py +291 -0
  108. glaip_sdk/tools/base.py +67 -14
  109. glaip_sdk/utils/__init__.py +1 -0
  110. glaip_sdk/utils/bundler.py +138 -2
  111. glaip_sdk/utils/import_resolver.py +43 -11
  112. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  113. glaip_sdk/utils/runtime_config.py +15 -12
  114. glaip_sdk/utils/sync.py +31 -11
  115. glaip_sdk/utils/tool_detection.py +274 -6
  116. glaip_sdk/utils/tool_storage_provider.py +140 -0
  117. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
  118. glaip_sdk-0.7.12.dist-info/RECORD +219 -0
  119. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
  120. glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
  121. glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
  122. glaip_sdk/cli/commands/agents.py +0 -1509
  123. glaip_sdk/cli/commands/mcps.py +0 -1356
  124. glaip_sdk/cli/commands/tools.py +0 -576
  125. glaip_sdk/cli/utils.py +0 -263
  126. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  127. glaip_sdk-0.6.5b6.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,282 @@ 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
+ self._maybe_show_update_prompt()
337
+ return True
338
+
339
+ def _update_pulse_step(
340
+ self,
341
+ state: AnimationState,
342
+ animator: LogoAnimator,
343
+ ) -> bool:
344
+ """Update pulse step and direction.
345
+
346
+ Args:
347
+ state: Animation state container.
348
+ animator: LogoAnimator instance for animation.
349
+
350
+ Returns:
351
+ True if animation should continue, False if should stop.
352
+ """
353
+ state.pulse_step[0] += state.pulse_direction[0] * state.step_size[0]
354
+ if state.pulse_step[0] >= animator.max_width + 5:
355
+ state.pulse_step[0] = animator.max_width + 5
356
+ state.pulse_direction[0] = -1
357
+ return not state.stop_requested.is_set()
358
+ if state.pulse_step[0] <= -5:
359
+ state.pulse_step[0] = -5
360
+ state.pulse_direction[0] = 1
361
+ return not state.stop_requested.is_set()
362
+ return True
363
+
364
+ def _create_animation_updater(
365
+ self,
366
+ animator: LogoAnimator,
367
+ state: AnimationState,
368
+ heading: Text,
369
+ ) -> Callable[[Live], None]:
370
+ """Create animation update function for background thread.
371
+
372
+ Args:
373
+ animator: LogoAnimator instance for animation.
374
+ state: Animation state container.
375
+ heading: Text heading for frames.
376
+
377
+ Returns:
378
+ Function to update animation in background thread.
379
+ """
380
+
381
+ def build_frame(step: int, status_text: str) -> Group:
382
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
383
+
384
+ def update_animation(live: Live) -> None:
385
+ """Update animation in background thread."""
386
+ while state.animation_running.is_set():
387
+ # Calculate next step
388
+ if not self._update_pulse_step(state, animator):
389
+ break
390
+
391
+ # Update frame with current status
392
+ try:
393
+ live.update(build_frame(state.pulse_step[0], state.current_status[0]))
394
+ except Exception:
395
+ # Animation may be stopped, ignore errors
396
+ break
397
+ time.sleep(self.ANIMATION_FRAME_DURATION)
398
+ state.animation_running.clear()
399
+
400
+ return update_animation
401
+
402
+ def _stop_animation_thread(
403
+ self,
404
+ animation_thread: threading.Thread,
405
+ state: AnimationState,
406
+ ) -> None:
407
+ """Stop animation thread gracefully.
408
+
409
+ Args:
410
+ animation_thread: Thread running animation.
411
+ state: Animation state container.
412
+ """
413
+ state.stop_requested.set()
414
+ state.step_size[0] = 3
415
+ animation_thread.join(timeout=1.5)
416
+ if animation_thread.is_alive():
417
+ state.animation_running.clear()
418
+ animation_thread.join(timeout=0.2)
419
+
420
+ def _run_animated_initialization(
421
+ self,
422
+ live: Live,
423
+ animator: LogoAnimator,
424
+ state: AnimationState,
425
+ heading: Text,
426
+ banner: Text,
427
+ ) -> bool:
428
+ """Run initialization tasks with animated logo.
429
+
430
+ Args:
431
+ live: Live context for animation updates.
432
+ animator: LogoAnimator instance for animation.
433
+ state: Animation state container.
434
+ heading: Text heading for frames.
435
+ banner: Text banner for final display.
436
+
437
+ Returns:
438
+ True if configuration is available, False otherwise.
439
+ """
440
+
441
+ def build_banner() -> Group:
442
+ return Group(heading, Text(""), banner)
443
+
444
+ update_animation = self._create_animation_updater(
445
+ animator,
446
+ state,
447
+ heading,
448
+ )
449
+
450
+ # Start animation thread.
451
+ animation_thread = threading.Thread(target=update_animation, args=(live,), daemon=True)
452
+ animation_thread.start()
453
+
454
+ # Small delay to ensure animation starts.
455
+ time.sleep(self.ANIMATION_STARTUP_DELAY)
456
+
457
+ # Run initialization tasks.
458
+ if not self._run_initialization_tasks(state.current_status, state.animation_running, status_callback=None):
459
+ return False
460
+
461
+ # Stop animation and show final banner.
462
+ self._stop_animation_thread(animation_thread, state)
463
+ live.update(build_banner())
464
+ return True
465
+
466
+ def _run_with_animated_logo(self, animator: LogoAnimator) -> bool:
467
+ """Run initialization with animated logo.
468
+
469
+ Args:
470
+ animator: LogoAnimator instance for animation.
471
+
472
+ Returns:
473
+ True if configuration is available, False otherwise.
474
+ """
475
+ state = AnimationState(
476
+ pulse_step=[0], # Use list for mutable shared state.
477
+ pulse_direction=[1], # Use list for mutable shared state.
478
+ step_size=[1],
479
+ current_status=[self.INITIALIZING_STATUS],
480
+ animation_running=threading.Event(),
481
+ stop_requested=threading.Event(),
482
+ )
483
+ state.animation_running.set()
484
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
485
+ banner = Text.from_markup(self._branding.get_welcome_banner())
486
+
487
+ def build_frame(step: int, status_text: str) -> Group:
488
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
489
+
490
+ try:
491
+ with Live(
492
+ build_frame(0, state.current_status[0]),
493
+ console=self.console,
494
+ refresh_per_second=self.ANIMATION_FPS,
495
+ transient=False,
496
+ ) as live:
497
+ return self._run_animated_initialization(
498
+ live,
499
+ animator,
500
+ state,
501
+ heading,
502
+ banner,
503
+ )
504
+ except KeyboardInterrupt:
505
+ # Graceful exit on Ctrl+C
506
+ state.animation_running.clear()
507
+ # Align with static path: show heading and cancellation message
508
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
509
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
510
+ return False
511
+
512
+ def _run_with_static_logo(self, animator: LogoAnimator) -> bool:
513
+ """Run initialization with static logo (non-TTY or NO_COLOR).
514
+
515
+ Args:
516
+ animator: LogoAnimator instance for static display.
517
+
518
+ Returns:
519
+ True if configuration is available, False otherwise.
520
+ """
521
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
522
+ banner = Text.from_markup(self._branding.get_welcome_banner())
523
+
524
+ def build_frame(status_text: str) -> Group:
525
+ return Group(heading, Text(""), animator.static_frame(status_text))
526
+
527
+ def build_banner() -> Group:
528
+ return Group(heading, Text(""), banner)
529
+
530
+ try:
531
+ with Live(
532
+ build_frame(self.INITIALIZING_STATUS),
533
+ console=self.console,
534
+ refresh_per_second=4,
535
+ transient=False,
536
+ ) as live:
537
+ # Run initialization tasks with status updates, reusing shared logic.
538
+ current_status = [self.INITIALIZING_STATUS]
539
+ animation_running = threading.Event()
540
+ animation_running.set()
541
+
542
+ # Update Live display when status changes via callback.
543
+ def update_display(status: str) -> None:
544
+ """Update Live display with current status."""
545
+ live.update(build_frame(status))
546
+
547
+ if not self._run_initialization_tasks(
548
+ current_status, animation_running, status_callback=update_display
549
+ ):
550
+ return False
551
+
552
+ live.update(build_banner())
553
+ return True
554
+ except KeyboardInterrupt:
555
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
556
+ return False
557
+
244
558
  def _run_interactive_loop(self) -> None:
245
559
  """Run the main interactive command loop."""
246
560
  while not self._should_exit:
@@ -548,7 +862,7 @@ class SlashSession:
548
862
  try:
549
863
  # Use the modern account-aware wizard directly (bypasses legacy config gating)
550
864
  _configure_interactive(account_name=None)
551
- self._config_cache = None
865
+ self.on_account_switched()
552
866
  if self._suppress_login_layout:
553
867
  self._welcome_rendered = False
554
868
  self._default_actions_shown = False
@@ -1211,6 +1525,33 @@ class SlashSession:
1211
1525
  self._client = get_client(self.ctx)
1212
1526
  return self._client
1213
1527
 
1528
+ def on_account_switched(self, _account_name: str | None = None) -> None:
1529
+ """Reset any state that depends on the active account.
1530
+
1531
+ The active account can change via `/accounts` (or other flows that call
1532
+ AccountStore.set_active_account). The slash session caches a configured
1533
+ client instance, so we must invalidate it to avoid leaking the previous
1534
+ account's API URL/key into subsequent commands like `/agents` or `/runs`.
1535
+
1536
+ This method clears:
1537
+ - Client and config cache (account-specific credentials)
1538
+ - Current agent and recent agents (agent data is account-scoped)
1539
+ - Runs pagination state (runs are account-scoped)
1540
+ - Active renderer and transcript ready state (UI state tied to account context)
1541
+ - Contextual commands (may be account-specific)
1542
+
1543
+ These broader resets ensure a clean slate when switching accounts, preventing
1544
+ stale data from the previous account from appearing in the new account's context.
1545
+ """
1546
+ self._client = None
1547
+ self._config_cache = None
1548
+ self._current_agent = None
1549
+ self.recent_agents = []
1550
+ self._runs_pagination_state.clear()
1551
+ self.clear_active_renderer()
1552
+ self.clear_agent_transcript_ready()
1553
+ self.set_contextual_commands(None)
1554
+
1214
1555
  def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
1215
1556
  """Set context-specific commands that should appear in completions."""
1216
1557
  self._contextual_commands = dict(commands or {})
@@ -1246,6 +1587,7 @@ class SlashSession:
1246
1587
  *,
1247
1588
  focus_agent: bool = False,
1248
1589
  initial: bool = False,
1590
+ show_branding: bool = True,
1249
1591
  ) -> None:
1250
1592
  """Render the session header with branding and status.
1251
1593
 
@@ -1253,14 +1595,16 @@ class SlashSession:
1253
1595
  active_agent: Optional active agent to display.
1254
1596
  focus_agent: Whether to focus on agent display.
1255
1597
  initial: Whether this is the initial render.
1598
+ show_branding: Whether to render the branding banner.
1256
1599
  """
1257
1600
  if focus_agent and active_agent is not None:
1258
1601
  self._render_focused_agent_header(active_agent)
1259
1602
  return
1260
1603
 
1261
1604
  full_header = initial or not self._welcome_rendered
1262
- if full_header:
1605
+ if full_header and show_branding:
1263
1606
  self._render_branding_banner()
1607
+ if full_header:
1264
1608
  self.console.rule(style=PRIMARY)
1265
1609
  self._render_main_header(active_agent, full=full_header)
1266
1610
  if full_header:
@@ -1270,7 +1614,7 @@ class SlashSession:
1270
1614
  def _render_branding_banner(self) -> None:
1271
1615
  """Render the GL AIP branding banner."""
1272
1616
  banner = self._branding.get_welcome_banner()
1273
- heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
1617
+ heading = self.CLI_HEADING_MARKUP
1274
1618
  self.console.print(heading)
1275
1619
  self.console.print()
1276
1620
  self.console.print(banner)
@@ -1340,14 +1684,16 @@ class SlashSession:
1340
1684
  f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
1341
1685
  )
1342
1686
  status_line = f"[{SUCCESS_STYLE}]ready[/]"
1343
- status_line += " · transcript ready" if transcript_status["transcript_ready"] else " · transcript pending"
1687
+ if not transcript_status["has_transcript"]:
1688
+ status_line += " · no transcript"
1689
+ elif transcript_status["transcript_ready"]:
1690
+ status_line += " · transcript ready"
1691
+ else:
1692
+ status_line += " · transcript pending"
1344
1693
  header_grid.add_row(primary_line, status_line)
1345
1694
 
1346
1695
  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]", "")
1696
+ header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
1351
1697
 
1352
1698
  return header_grid
1353
1699
 
@@ -1,9 +1,34 @@
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.keybind_registry import (
6
+ Keybind,
7
+ KeybindRegistry,
8
+ format_key_sequence,
9
+ parse_key_sequence,
10
+ )
11
+ from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
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
8
18
 
9
- __all__ = ["RemoteRunsTextualApp", "RemoteRunsTUICallbacks", "run_remote_runs_textual"]
19
+ __all__ = [
20
+ "TUIContext",
21
+ "ToastBus",
22
+ "ToastVariant",
23
+ "TerminalCapabilities",
24
+ "detect_terminal_background",
25
+ "RemoteRunsTextualApp",
26
+ "RemoteRunsTUICallbacks",
27
+ "run_remote_runs_textual",
28
+ "KeybindRegistry",
29
+ "Keybind",
30
+ "parse_key_sequence",
31
+ "format_key_sequence",
32
+ "ClipboardAdapter",
33
+ "ClipboardResult",
34
+ ]
@@ -3,6 +3,18 @@
3
3
  * Keep layout compact: filter sits tight above the table; header shows active account.
4
4
  */
5
5
 
6
+ Screen {
7
+ layers: base toasts;
8
+ }
9
+
10
+ #toast-container {
11
+ width: 100%;
12
+ height: auto;
13
+ dock: top;
14
+ align: right top;
15
+ layer: toasts;
16
+ }
17
+
6
18
  #header-info {
7
19
  padding: 0 1 0 1;
8
20
  margin: 0;
@@ -11,7 +23,7 @@
11
23
 
12
24
  #env-lock {
13
25
  padding: 0 1 0 1;
14
- color: yellow;
26
+ color: $warning;
15
27
  height: 1;
16
28
  }
17
29
 
@@ -45,6 +57,7 @@
45
57
  padding: 0 1 0 1;
46
58
  margin: 0 0 0 0;
47
59
  height: 1fr;
60
+ border: tall $primary;
48
61
  }
49
62
 
50
63
  #status-bar {
@@ -58,11 +71,12 @@
58
71
  }
59
72
 
60
73
  #status {
61
- padding: 0 1 0 1;
62
- margin: 0;
63
- color: cyan;
74
+ height: 3;
75
+ padding: 0 1;
76
+ color: $secondary;
64
77
  }
65
78
 
79
+
66
80
  .form-label {
67
81
  padding: 0 1 0 1;
68
82
  }
@@ -73,7 +87,7 @@
73
87
 
74
88
  #form-status, #confirm-status {
75
89
  padding: 0 1;
76
- color: yellow;
90
+ color: $warning;
77
91
  }
78
92
 
79
93
  #form-test {
@@ -84,3 +98,63 @@
84
98
  #form-actions {
85
99
  margin: 0 1 0 1;
86
100
  }
101
+
102
+ /* Harlequin Layout Styling */
103
+
104
+ #left-pane-title, #right-pane-title {
105
+ padding: 1;
106
+ text-style: bold;
107
+ border-bottom: solid $primary;
108
+ height: 3;
109
+ }
110
+
111
+ #harlequin-filter {
112
+ padding: 0 1;
113
+ margin: 1;
114
+ height: 3;
115
+ }
116
+
117
+ #harlequin-accounts-list {
118
+ padding: 0 1;
119
+ margin: 1;
120
+ height: 1fr;
121
+ }
122
+
123
+ #left-content {
124
+ height: 100%;
125
+ }
126
+
127
+ #right-content {
128
+ padding: 1;
129
+ height: 100%;
130
+ }
131
+
132
+ .detail-label {
133
+ padding: 0 1 0 0;
134
+ text-style: bold;
135
+ width: 12;
136
+ }
137
+
138
+ #detail-fields {
139
+ padding: 1 0;
140
+ height: auto;
141
+ }
142
+
143
+ #harlequin-detail-url,
144
+ #harlequin-detail-key,
145
+ #harlequin-detail-status {
146
+ padding: 0 1;
147
+ margin-bottom: 1;
148
+ }
149
+
150
+ #harlequin-detail-actions {
151
+ padding: 1 0;
152
+ margin-top: 1;
153
+ height: auto;
154
+ }
155
+
156
+ #harlequin-status {
157
+ padding: 1;
158
+ margin-top: 1;
159
+ height: auto;
160
+ }