glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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 (135) hide show
  1. glaip_sdk/agents/base.py +283 -30
  2. glaip_sdk/agents/component.py +233 -0
  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 +1 -1
  17. glaip_sdk/cli/commands/configure.py +1 -2
  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/entrypoint.py +20 -0
  46. glaip_sdk/cli/main.py +112 -35
  47. glaip_sdk/cli/pager.py +3 -3
  48. glaip_sdk/cli/resolution.py +2 -1
  49. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  50. glaip_sdk/cli/slash/agent_session.py +1 -1
  51. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  52. glaip_sdk/cli/slash/session.py +343 -20
  53. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  54. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  55. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  56. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  57. glaip_sdk/cli/slash/tui/context.py +92 -0
  58. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  59. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  60. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  61. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  62. glaip_sdk/cli/slash/tui/loading.py +43 -21
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  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 +388 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +1 -1
  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 +293 -17
  78. glaip_sdk/client/base.py +25 -0
  79. glaip_sdk/client/hitl.py +136 -0
  80. glaip_sdk/client/main.py +7 -5
  81. glaip_sdk/client/mcps.py +44 -13
  82. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  83. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  84. glaip_sdk/client/payloads/agent/responses.py +43 -0
  85. glaip_sdk/client/run_rendering.py +109 -30
  86. glaip_sdk/client/schedules.py +439 -0
  87. glaip_sdk/client/tools.py +52 -23
  88. glaip_sdk/config/constants.py +22 -2
  89. glaip_sdk/guardrails/__init__.py +80 -0
  90. glaip_sdk/guardrails/serializer.py +91 -0
  91. glaip_sdk/hitl/__init__.py +35 -2
  92. glaip_sdk/hitl/base.py +64 -0
  93. glaip_sdk/hitl/callback.py +43 -0
  94. glaip_sdk/hitl/local.py +1 -31
  95. glaip_sdk/hitl/remote.py +523 -0
  96. glaip_sdk/models/__init__.py +47 -1
  97. glaip_sdk/models/_provider_mappings.py +101 -0
  98. glaip_sdk/models/_validation.py +97 -0
  99. glaip_sdk/models/agent.py +2 -1
  100. glaip_sdk/models/agent_runs.py +2 -1
  101. glaip_sdk/models/constants.py +141 -0
  102. glaip_sdk/models/model.py +170 -0
  103. glaip_sdk/models/schedule.py +224 -0
  104. glaip_sdk/payload_schemas/agent.py +1 -0
  105. glaip_sdk/payload_schemas/guardrails.py +34 -0
  106. glaip_sdk/ptc.py +145 -0
  107. glaip_sdk/registry/tool.py +270 -57
  108. glaip_sdk/runner/__init__.py +20 -3
  109. glaip_sdk/runner/deps.py +4 -1
  110. glaip_sdk/runner/langgraph.py +251 -27
  111. glaip_sdk/runner/logging_config.py +77 -0
  112. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  113. glaip_sdk/runner/ptc_adapter.py +98 -0
  114. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  115. glaip_sdk/schedules/__init__.py +22 -0
  116. glaip_sdk/schedules/base.py +291 -0
  117. glaip_sdk/tools/base.py +67 -14
  118. glaip_sdk/utils/__init__.py +1 -0
  119. glaip_sdk/utils/agent_config.py +8 -2
  120. glaip_sdk/utils/bundler.py +138 -2
  121. glaip_sdk/utils/import_resolver.py +427 -49
  122. glaip_sdk/utils/runtime_config.py +3 -2
  123. glaip_sdk/utils/sync.py +31 -11
  124. glaip_sdk/utils/tool_detection.py +274 -6
  125. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
  126. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  127. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
  128. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  129. glaip_sdk/cli/commands/agents.py +0 -1509
  130. glaip_sdk/cli/commands/mcps.py +0 -1356
  131. glaip_sdk/cli/commands/tools.py +0 -576
  132. glaip_sdk/cli/utils.py +0 -263
  133. glaip_sdk-0.6.19.dist-info/RECORD +0 -163
  134. glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
  135. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
@@ -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,19 @@ class SlashSession:
229
260
  self._run_non_interactive(initial_commands)
230
261
  return
231
262
 
232
- if not self._ensure_configuration():
263
+ self._maybe_show_update_prompt()
264
+ # Use animated logo during initialization if supported
265
+ animator = LogoAnimator(console=self.console)
266
+ if animator.should_animate() and self._interactive:
267
+ config_available = self._run_with_animated_logo(animator)
268
+ else:
269
+ # Fallback to static logo for non-TTY or NO_COLOR
270
+ config_available = self._run_with_static_logo(animator)
271
+
272
+ if not config_available:
233
273
  return
234
274
 
235
- self._maybe_show_update_prompt()
236
- self._render_header(initial=not self._welcome_rendered)
275
+ self._render_header(initial=not self._welcome_rendered, show_branding=False)
237
276
  if not self._default_actions_shown:
238
277
  self._show_default_quick_actions()
239
278
  self._run_interactive_loop()
@@ -241,6 +280,284 @@ class SlashSession:
241
280
  if ctx_obj is not None:
242
281
  restore_slash_session_context(ctx_obj, previous_session)
243
282
 
283
+ def _initialize_tui_context(self) -> None:
284
+ """Initialize TUI context with error handling.
285
+
286
+ Sets self.tui_ctx to None if initialization fails.
287
+ """
288
+ try:
289
+ self.tui_ctx = asyncio.run(TUIContext.create(detect_osc11=False))
290
+ except RuntimeError:
291
+ try:
292
+ loop = asyncio.get_event_loop()
293
+ except RuntimeError:
294
+ self.tui_ctx = None
295
+ else:
296
+ if loop.is_running():
297
+ self.tui_ctx = None
298
+ else:
299
+ self.tui_ctx = loop.run_until_complete(TUIContext.create(detect_osc11=False))
300
+ except Exception:
301
+ self.tui_ctx = None
302
+
303
+ def _run_initialization_tasks(
304
+ self,
305
+ current_status: list[str],
306
+ animation_running: threading.Event,
307
+ status_callback: Callable[[str], None] | None = None,
308
+ ) -> bool:
309
+ """Run initialization tasks with status updates.
310
+
311
+ Args:
312
+ current_status: Mutable list with current status message.
313
+ animation_running: Event to signal animation state.
314
+ status_callback: Optional callback to invoke when status changes.
315
+
316
+ Returns:
317
+ True if configuration is available, False otherwise.
318
+ """
319
+ # Task 1: TUI Context.
320
+ current_status[0] = "Detecting terminal..."
321
+ if status_callback:
322
+ status_callback(current_status[0])
323
+ self._initialize_tui_context()
324
+
325
+ # Task 2: Configuration.
326
+ current_status[0] = "Connecting to API..."
327
+ if status_callback:
328
+ status_callback(current_status[0])
329
+ if not self._ensure_configuration():
330
+ animation_running.clear()
331
+ return False
332
+
333
+ return True
334
+
335
+ def _update_pulse_step(
336
+ self,
337
+ state: AnimationState,
338
+ animator: LogoAnimator,
339
+ ) -> bool:
340
+ """Update pulse step and direction.
341
+
342
+ Args:
343
+ state: Animation state container.
344
+ animator: LogoAnimator instance for animation.
345
+
346
+ Returns:
347
+ True if animation should continue, False if should stop.
348
+ """
349
+ state.pulse_step[0] += state.pulse_direction[0] * state.step_size[0]
350
+ if state.pulse_step[0] >= animator.max_width + 5:
351
+ state.pulse_step[0] = animator.max_width + 5
352
+ state.pulse_direction[0] = -1
353
+ return not state.stop_requested.is_set()
354
+ if state.pulse_step[0] <= -5:
355
+ state.pulse_step[0] = -5
356
+ state.pulse_direction[0] = 1
357
+ return not state.stop_requested.is_set()
358
+ return True
359
+
360
+ def _create_animation_updater(
361
+ self,
362
+ animator: LogoAnimator,
363
+ state: AnimationState,
364
+ heading: Text,
365
+ ) -> Callable[[Live], None]:
366
+ """Create animation update function for background thread.
367
+
368
+ Args:
369
+ animator: LogoAnimator instance for animation.
370
+ state: Animation state container.
371
+ heading: Text heading for frames.
372
+
373
+ Returns:
374
+ Function to update animation in background thread.
375
+ """
376
+
377
+ def build_frame(step: int, status_text: str) -> Group:
378
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
379
+
380
+ def update_animation(live: Live) -> None:
381
+ """Update animation in background thread."""
382
+ while state.animation_running.is_set():
383
+ # Calculate next step
384
+ if not self._update_pulse_step(state, animator):
385
+ break
386
+
387
+ # Update frame with current status
388
+ try:
389
+ live.update(build_frame(state.pulse_step[0], state.current_status[0]))
390
+ except Exception:
391
+ # Animation may be stopped, ignore errors
392
+ break
393
+ time.sleep(self.ANIMATION_FRAME_DURATION)
394
+ state.animation_running.clear()
395
+
396
+ return update_animation
397
+
398
+ def _stop_animation_thread(
399
+ self,
400
+ animation_thread: threading.Thread,
401
+ state: AnimationState,
402
+ ) -> None:
403
+ """Stop animation thread gracefully.
404
+
405
+ Args:
406
+ animation_thread: Thread running animation.
407
+ state: Animation state container.
408
+ """
409
+ state.stop_requested.set()
410
+ state.step_size[0] = 3
411
+ animation_thread.join(timeout=1.5)
412
+ if animation_thread.is_alive():
413
+ state.animation_running.clear()
414
+ animation_thread.join(timeout=0.2)
415
+
416
+ def _run_animated_initialization(
417
+ self,
418
+ live: Live,
419
+ animator: LogoAnimator,
420
+ state: AnimationState,
421
+ heading: Text,
422
+ banner: Text,
423
+ ) -> bool:
424
+ """Run initialization tasks with animated logo.
425
+
426
+ Args:
427
+ live: Live context for animation updates.
428
+ animator: LogoAnimator instance for animation.
429
+ state: Animation state container.
430
+ heading: Text heading for frames.
431
+ banner: Text banner for final display.
432
+
433
+ Returns:
434
+ True if configuration is available, False otherwise.
435
+ """
436
+
437
+ def build_banner() -> Group:
438
+ return Group(heading, Text(""), banner)
439
+
440
+ update_animation = self._create_animation_updater(
441
+ animator,
442
+ state,
443
+ heading,
444
+ )
445
+
446
+ # Start animation thread.
447
+ animation_thread = threading.Thread(target=update_animation, args=(live,), daemon=True)
448
+ animation_thread.start()
449
+
450
+ # Small delay to ensure animation starts.
451
+ time.sleep(self.ANIMATION_STARTUP_DELAY)
452
+
453
+ def update_status(status: str) -> None:
454
+ state.current_status[0] = status
455
+
456
+ # Run initialization tasks.
457
+ if not self._run_initialization_tasks(
458
+ state.current_status,
459
+ state.animation_running,
460
+ status_callback=update_status,
461
+ ):
462
+ return False
463
+
464
+ # Stop animation and show final banner.
465
+ self._stop_animation_thread(animation_thread, state)
466
+ live.update(build_banner())
467
+ return True
468
+
469
+ def _run_with_animated_logo(self, animator: LogoAnimator) -> bool:
470
+ """Run initialization with animated logo.
471
+
472
+ Args:
473
+ animator: LogoAnimator instance for animation.
474
+
475
+ Returns:
476
+ True if configuration is available, False otherwise.
477
+ """
478
+ state = AnimationState(
479
+ pulse_step=[0], # Use list for mutable shared state.
480
+ pulse_direction=[1], # Use list for mutable shared state.
481
+ step_size=[1],
482
+ current_status=[self.INITIALIZING_STATUS],
483
+ animation_running=threading.Event(),
484
+ stop_requested=threading.Event(),
485
+ )
486
+ state.animation_running.set()
487
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
488
+ banner = Text.from_markup(self._branding.get_welcome_banner())
489
+
490
+ def build_frame(step: int, status_text: str) -> Group:
491
+ return Group(heading, Text(""), animator.generate_frame(step, status_text))
492
+
493
+ try:
494
+ with Live(
495
+ build_frame(0, state.current_status[0]),
496
+ console=self.console,
497
+ refresh_per_second=self.ANIMATION_FPS,
498
+ transient=False,
499
+ ) as live:
500
+ return self._run_animated_initialization(
501
+ live,
502
+ animator,
503
+ state,
504
+ heading,
505
+ banner,
506
+ )
507
+ except KeyboardInterrupt:
508
+ # Graceful exit on Ctrl+C
509
+ state.animation_running.clear()
510
+ # Align with static path: show heading and cancellation message
511
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
512
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
513
+ return False
514
+
515
+ def _run_with_static_logo(self, animator: LogoAnimator) -> bool:
516
+ """Run initialization with static logo (non-TTY or NO_COLOR).
517
+
518
+ Args:
519
+ animator: LogoAnimator instance for static display.
520
+
521
+ Returns:
522
+ True if configuration is available, False otherwise.
523
+ """
524
+ heading = Text.from_markup(self.CLI_HEADING_MARKUP)
525
+ banner = Text.from_markup(self._branding.get_welcome_banner())
526
+
527
+ def build_frame(status_text: str) -> Group:
528
+ return Group(heading, Text(""), animator.static_frame(status_text))
529
+
530
+ def build_banner() -> Group:
531
+ return Group(heading, Text(""), banner)
532
+
533
+ try:
534
+ with Live(
535
+ build_frame(self.INITIALIZING_STATUS),
536
+ console=self.console,
537
+ refresh_per_second=4,
538
+ transient=False,
539
+ ) as live:
540
+ # Run initialization tasks with status updates, reusing shared logic.
541
+ current_status = [self.INITIALIZING_STATUS]
542
+ animation_running = threading.Event()
543
+ animation_running.set()
544
+
545
+ # Update Live display when status changes via callback.
546
+ def update_display(status: str) -> None:
547
+ """Update Live display with current status."""
548
+ live.update(build_frame(status))
549
+
550
+ if not self._run_initialization_tasks(
551
+ current_status, animation_running, status_callback=update_display
552
+ ):
553
+ return False
554
+
555
+ live.update(build_banner())
556
+ return True
557
+ except KeyboardInterrupt:
558
+ self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
559
+ return False
560
+
244
561
  def _run_interactive_loop(self) -> None:
245
562
  """Run the main interactive command loop."""
246
563
  while not self._should_exit:
@@ -900,7 +1217,7 @@ class SlashSession:
900
1217
  self._register(
901
1218
  SlashCommand(
902
1219
  name="accounts",
903
- help="✨ NEW · Browse and switch stored accounts (Textual with Rich fallback).",
1220
+ help="✨ NEW · Browse and switch stored accounts.",
904
1221
  handler=SlashSession._cmd_accounts,
905
1222
  )
906
1223
  )
@@ -1273,6 +1590,7 @@ class SlashSession:
1273
1590
  *,
1274
1591
  focus_agent: bool = False,
1275
1592
  initial: bool = False,
1593
+ show_branding: bool = True,
1276
1594
  ) -> None:
1277
1595
  """Render the session header with branding and status.
1278
1596
 
@@ -1280,14 +1598,16 @@ class SlashSession:
1280
1598
  active_agent: Optional active agent to display.
1281
1599
  focus_agent: Whether to focus on agent display.
1282
1600
  initial: Whether this is the initial render.
1601
+ show_branding: Whether to render the branding banner.
1283
1602
  """
1284
1603
  if focus_agent and active_agent is not None:
1285
1604
  self._render_focused_agent_header(active_agent)
1286
1605
  return
1287
1606
 
1288
1607
  full_header = initial or not self._welcome_rendered
1289
- if full_header:
1608
+ if full_header and show_branding:
1290
1609
  self._render_branding_banner()
1610
+ if full_header:
1291
1611
  self.console.rule(style=PRIMARY)
1292
1612
  self._render_main_header(active_agent, full=full_header)
1293
1613
  if full_header:
@@ -1297,14 +1617,17 @@ class SlashSession:
1297
1617
  def _render_branding_banner(self) -> None:
1298
1618
  """Render the GL AIP branding banner."""
1299
1619
  banner = self._branding.get_welcome_banner()
1300
- heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
1620
+ heading = self.CLI_HEADING_MARKUP
1301
1621
  self.console.print(heading)
1302
1622
  self.console.print()
1303
1623
  self.console.print(banner)
1304
1624
 
1305
- def _maybe_show_update_prompt(self) -> None:
1625
+ def _maybe_show_update_prompt(self, *, defer: bool = False) -> None:
1306
1626
  """Display update prompt once per session when applicable."""
1307
- if self._update_prompt_shown:
1627
+ if self._update_prompt_shown or (defer and not self._update_prompt_shown):
1628
+ if defer:
1629
+ # Just mark as ready to show, but don't show yet
1630
+ return
1308
1631
  return
1309
1632
 
1310
1633
  self._update_notifier(
@@ -1,9 +1,37 @@
1
1
  """Textual UI helpers for slash commands."""
2
2
 
3
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardReadResult, 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
+ "ClipboardReadResult",
35
+ "ClipboardResult",
36
+ "PulseIndicator",
37
+ ]
@@ -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
 
@@ -41,10 +53,23 @@
41
53
  margin-left: 1;
42
54
  }
43
55
 
56
+ Button:hover {
57
+ background: $surface-lighten-1;
58
+ }
59
+
44
60
  #accounts-table {
45
61
  padding: 0 1 0 1;
46
62
  margin: 0 0 0 0;
47
63
  height: 1fr;
64
+ border: tall $primary;
65
+ }
66
+
67
+ #accounts-table > .datatable--row:hover {
68
+ background: $surface-lighten-1;
69
+ }
70
+
71
+ .sidebar-block:hover {
72
+ background: $surface-lighten-1;
48
73
  }
49
74
 
50
75
  #status-bar {
@@ -54,15 +79,15 @@
54
79
 
55
80
  #accounts-loading {
56
81
  width: 8;
57
- display: none;
58
82
  }
59
83
 
60
84
  #status {
61
- padding: 0 1 0 1;
62
- margin: 0;
63
- color: cyan;
85
+ height: 3;
86
+ padding: 0 1;
87
+ color: $secondary;
64
88
  }
65
89
 
90
+
66
91
  .form-label {
67
92
  padding: 0 1 0 1;
68
93
  }
@@ -73,7 +98,7 @@
73
98
 
74
99
  #form-status, #confirm-status {
75
100
  padding: 0 1;
76
- color: yellow;
101
+ color: $warning;
77
102
  }
78
103
 
79
104
  #form-test {
@@ -84,3 +109,69 @@
84
109
  #form-actions {
85
110
  margin: 0 1 0 1;
86
111
  }
112
+
113
+ /* Harlequin Layout Styling */
114
+
115
+ #left-pane-title, #right-pane-title {
116
+ padding: 1;
117
+ text-style: bold;
118
+ border-bottom: solid $primary;
119
+ height: 3;
120
+ }
121
+
122
+ #harlequin-filter {
123
+ padding: 0 1;
124
+ margin: 1;
125
+ height: 3;
126
+ }
127
+
128
+ #harlequin-accounts-list {
129
+ padding: 0 1;
130
+ margin: 1;
131
+ height: 1fr;
132
+ }
133
+
134
+ #left-content {
135
+ height: 100%;
136
+ }
137
+
138
+ #right-content {
139
+ padding: 1;
140
+ height: 100%;
141
+ }
142
+
143
+ .detail-label {
144
+ padding: 0 1 0 0;
145
+ text-style: bold;
146
+ width: 12;
147
+ }
148
+
149
+ #detail-fields {
150
+ padding: 1 0;
151
+ height: auto;
152
+ }
153
+
154
+ #harlequin-detail-url,
155
+ #harlequin-detail-key,
156
+ #harlequin-detail-status {
157
+ padding: 0 1;
158
+ margin-bottom: 1;
159
+ }
160
+
161
+ #harlequin-detail-actions {
162
+ padding: 1 0;
163
+ margin-top: 1;
164
+ height: auto;
165
+ }
166
+
167
+ #harlequin-loading {
168
+ width: auto;
169
+ height: 3;
170
+ padding: 0 1;
171
+ }
172
+
173
+ #harlequin-status {
174
+ padding: 1;
175
+ margin-top: 1;
176
+ height: auto;
177
+ }