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.
- glaip_sdk/__init__.py +42 -5
- glaip_sdk/agents/base.py +217 -42
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +369 -23
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +87 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +374 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +50 -8
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +414 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +318 -42
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +15 -12
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
- glaip_sdk-0.7.12.dist-info/RECORD +219 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
- glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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__ = [
|
|
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:
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
color:
|
|
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:
|
|
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
|
+
}
|