glaip-sdk 0.7.14__py3-none-any.whl → 0.7.16__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/agents/base.py CHANGED
@@ -202,7 +202,11 @@ class Agent:
202
202
 
203
203
  self._agent_config = kwargs.pop("agent_config", Agent._UNSET) # type: ignore[assignment]
204
204
  self._tool_configs = kwargs.pop("tool_configs", Agent._UNSET) # type: ignore[assignment]
205
- self._mcp_configs = kwargs.pop("mcp_configs", Agent._UNSET) # type: ignore[assignment]
205
+ mcp_configs = kwargs.pop("mcp_configs", Agent._UNSET)
206
+ if mcp_configs is not Agent._UNSET and isinstance(mcp_configs, dict):
207
+ self._mcp_configs = self._normalize_mcp_configs(mcp_configs)
208
+ else:
209
+ self._mcp_configs = mcp_configs # type: ignore[assignment]
206
210
  self._a2a_profile = kwargs.pop("a2a_profile", Agent._UNSET) # type: ignore[assignment]
207
211
 
208
212
  # Warn about unexpected kwargs
@@ -783,6 +787,47 @@ class Agent:
783
787
 
784
788
  return resolved
785
789
 
790
+ def _normalize_mcp_configs(self, mcp_configs: dict[Any, Any]) -> dict[Any, Any]:
791
+ """Normalize mcp_configs by wrapping misplaced transport keys in 'config'.
792
+
793
+ This ensures that flat transport settings (e.g. {'url': '...'}) provided
794
+ by the user are correctly moved into the 'config' block required by the
795
+ Platform, ensuring parity between local and remote execution.
796
+
797
+ Args:
798
+ mcp_configs: The raw mcp_configs dictionary.
799
+
800
+ Returns:
801
+ Normalized mcp_configs dictionary.
802
+ """
803
+ from glaip_sdk.runner.langgraph import _MCP_TRANSPORT_KEYS # noqa: PLC0415
804
+
805
+ normalized = {}
806
+ for mcp_key, override in mcp_configs.items():
807
+ if not isinstance(override, dict):
808
+ normalized[mcp_key] = override
809
+ continue
810
+
811
+ misplaced = {k: v for k, v in override.items() if k in _MCP_TRANSPORT_KEYS}
812
+
813
+ if misplaced:
814
+ new_override = override.copy()
815
+ config_block = new_override.get("config", {})
816
+ if not isinstance(config_block, dict):
817
+ config_block = {}
818
+
819
+ config_block.update(misplaced)
820
+ new_override["config"] = config_block
821
+
822
+ for k in misplaced:
823
+ new_override.pop(k, None)
824
+
825
+ normalized[mcp_key] = new_override
826
+ else:
827
+ normalized[mcp_key] = override
828
+
829
+ return normalized
830
+
786
831
  def _resolve_agents(self, registry: AgentRegistry) -> list[str]:
787
832
  """Resolve sub-agent references using AgentRegistry.
788
833
 
@@ -333,7 +333,8 @@ class SlashSession:
333
333
  current_status[0] = "Checking for updates..."
334
334
  if status_callback:
335
335
  status_callback(current_status[0])
336
- self._maybe_show_update_prompt()
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))
337
338
  return True
338
339
 
339
340
  def _update_pulse_step(
@@ -1619,9 +1620,12 @@ class SlashSession:
1619
1620
  self.console.print()
1620
1621
  self.console.print(banner)
1621
1622
 
1622
- def _maybe_show_update_prompt(self) -> None:
1623
+ def _maybe_show_update_prompt(self, *, defer: bool = False) -> None:
1623
1624
  """Display update prompt once per session when applicable."""
1624
- 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
1625
1629
  return
1626
1630
 
1627
1631
  self._update_notifier(
@@ -2,19 +2,20 @@
2
2
 
3
3
  from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
4
4
  from glaip_sdk.cli.slash.tui.context import TUIContext
5
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
5
6
  from glaip_sdk.cli.slash.tui.keybind_registry import (
6
7
  Keybind,
7
8
  KeybindRegistry,
8
9
  format_key_sequence,
9
10
  parse_key_sequence,
10
11
  )
11
- from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
12
12
  from glaip_sdk.cli.slash.tui.remote_runs_app import (
13
13
  RemoteRunsTextualApp,
14
14
  RemoteRunsTUICallbacks,
15
15
  run_remote_runs_textual,
16
16
  )
17
17
  from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_terminal_background
18
+ from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
18
19
 
19
20
  __all__ = [
20
21
  "TUIContext",
@@ -31,4 +32,5 @@ __all__ = [
31
32
  "format_key_sequence",
32
33
  "ClipboardAdapter",
33
34
  "ClipboardResult",
35
+ "PulseIndicator",
34
36
  ]
@@ -79,7 +79,6 @@ Button:hover {
79
79
 
80
80
  #accounts-loading {
81
81
  width: 8;
82
- display: none;
83
82
  }
84
83
 
85
84
  #status {
@@ -165,6 +164,12 @@ Button:hover {
165
164
  height: auto;
166
165
  }
167
166
 
167
+ #harlequin-loading {
168
+ width: auto;
169
+ height: 3;
170
+ padding: 0 1;
171
+ }
172
+
168
173
  #harlequin-status {
169
174
  padding: 1;
170
175
  margin-top: 1;
@@ -30,77 +30,40 @@ from glaip_sdk.cli.slash.accounts_shared import (
30
30
  from glaip_sdk.cli.slash.tui.background_tasks import BackgroundTaskMixin
31
31
  from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
32
32
  from glaip_sdk.cli.slash.tui.context import TUIContext
33
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
33
34
  from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
34
35
  from glaip_sdk.cli.slash.tui.layouts.harlequin import HarlequinScreen
35
36
  from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
36
37
  from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
37
38
  from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
38
39
 
39
- try: # pragma: no cover - optional dependency
40
- from glaip_sdk.cli.slash.tui.toast import (
41
- ClipboardToastMixin,
42
- Toast,
43
- ToastBus,
44
- ToastContainer,
45
- ToastHandlerMixin,
46
- ToastVariant,
47
- )
48
- except Exception: # pragma: no cover - optional dependency
49
- ClipboardToastMixin = object # type: ignore[assignment, misc]
50
- Toast = None # type: ignore[assignment]
51
- ToastBus = None # type: ignore[assignment]
52
- ToastContainer = None # type: ignore[assignment]
53
- ToastHandlerMixin = object # type: ignore[assignment, misc]
54
- ToastVariant = None # type: ignore[assignment]
40
+ from glaip_sdk.cli.slash.tui.toast import (
41
+ ClipboardToastMixin,
42
+ Toast,
43
+ ToastBus,
44
+ ToastContainer,
45
+ ToastHandlerMixin,
46
+ ToastVariant,
47
+ )
55
48
  from glaip_sdk.cli.validators import validate_api_key
56
49
  from glaip_sdk.utils.validation import validate_url
57
50
 
58
- try: # pragma: no cover - optional dependency
59
- from textual import events
60
- from textual.app import App, ComposeResult
61
- from textual.binding import Binding
62
- from textual.containers import Horizontal, Vertical
63
- from textual.coordinate import Coordinate
64
- from textual.screen import ModalScreen
65
- from textual.suggester import SuggestFromList
66
- from textual.widgets import Button, Checkbox, DataTable, Footer, Header, Input, LoadingIndicator, Static
67
- except Exception: # pragma: no cover - optional dependency
68
- events = None # type: ignore[assignment]
69
- App = None # type: ignore[assignment]
70
- ComposeResult = None # type: ignore[assignment]
71
- Binding = None # type: ignore[assignment]
72
- Horizontal = None # type: ignore[assignment]
73
- Vertical = None # type: ignore[assignment]
74
- Coordinate = None # type: ignore[assignment]
75
- Button = None # type: ignore[assignment]
76
- Checkbox = None # type: ignore[assignment]
77
- DataTable = None # type: ignore[assignment]
78
- Footer = None # type: ignore[assignment]
79
- Header = None # type: ignore[assignment]
80
- Input = None # type: ignore[assignment]
81
- LoadingIndicator = None # type: ignore[assignment]
82
- ModalScreen = None # type: ignore[assignment]
83
- Static = None # type: ignore[assignment]
84
- SuggestFromList = None # type: ignore[assignment]
85
- Theme = None # type: ignore[assignment]
86
-
87
- if App is not None:
88
- try: # pragma: no cover - optional dependency
89
- from textual.theme import Theme
90
- except Exception: # pragma: no cover - optional dependency
91
- Theme = None # type: ignore[assignment]
92
-
93
- TEXTUAL_SUPPORTED = App is not None and DataTable is not None
94
-
95
- # Use safe bases so the module remains importable without Textual installed.
96
- if TEXTUAL_SUPPORTED:
97
- _AccountFormBase = ModalScreen[dict[str, Any] | None]
98
- _ConfirmDeleteBase = ModalScreen[str | None]
99
- _AppBase = App[None]
100
- else:
101
- _AccountFormBase = object
102
- _ConfirmDeleteBase = object
103
- _AppBase = object
51
+ from textual.app import App, ComposeResult
52
+ from textual.binding import Binding
53
+ from textual.containers import Horizontal, Vertical
54
+ from textual.coordinate import Coordinate
55
+ from textual.screen import ModalScreen
56
+ from textual.suggester import SuggestFromList
57
+ from textual.theme import Theme
58
+ from textual.widgets import Button, Checkbox, DataTable, Footer, Input, Static
59
+
60
+ # Harlequin layout requires specific widget support
61
+ TEXTUAL_SUPPORTED = True
62
+
63
+ # Use standard Textual base classes
64
+ _AccountFormBase = ModalScreen[dict[str, Any] | None]
65
+ _ConfirmDeleteBase = ModalScreen[str | None]
66
+ _AppBase = App[None]
104
67
 
105
68
  # Widget IDs for Textual UI
106
69
  ACCOUNTS_TABLE_ID = "#accounts-table"
@@ -390,6 +353,10 @@ class AccountFormModal(_AccountFormBase): # pragma: no cover - interactive
390
353
  if btn_id == "form-save":
391
354
  self._handle_submit()
392
355
 
356
+ def on_input_submitted(self, _event: Input.Submitted) -> None:
357
+ """Handle Enter key to save."""
358
+ self._handle_submit()
359
+
393
360
  def _handle_submit(self) -> None:
394
361
  """Validate inputs and dismiss with payload on success."""
395
362
  status = self.query_one("#form-status", Static)
@@ -457,6 +424,10 @@ class ConfirmDeleteModal(_ConfirmDeleteBase): # pragma: no cover - interactive
457
424
  if btn_id == "confirm-delete":
458
425
  self._handle_confirm()
459
426
 
427
+ def on_input_submitted(self, _event: Input.Submitted) -> None:
428
+ """Handle Enter key in confirmation input."""
429
+ self._handle_confirm()
430
+
460
431
  def _handle_confirm(self) -> None:
461
432
  """Dismiss with name when confirmation matches."""
462
433
  status = self.query_one("#confirm-status", Static)
@@ -520,6 +491,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
520
491
  ctx: Shared TUI context.
521
492
  """
522
493
  super().__init__(ctx=ctx)
494
+ self._ctx = ctx
523
495
  self._store = get_account_store()
524
496
  self._all_rows = rows
525
497
  self._active_account = active_account
@@ -562,6 +534,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
562
534
  yield Button("(e) Edit", id="action-edit")
563
535
  yield Button("(d) Delete", id="action-delete")
564
536
  yield Button("(c) Copy", id="action-copy")
537
+ yield PulseIndicator(id="harlequin-loading")
565
538
  yield Static("", id="harlequin-status")
566
539
  # Help text showing keyboard shortcuts at the bottom
567
540
  yield Static(
@@ -588,6 +561,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
588
561
  self._prepare_toasts()
589
562
  self._register_keybinds()
590
563
  self._update_detail_pane()
564
+ self._hide_loading()
591
565
 
592
566
  def _initialize_context_services(self) -> None:
593
567
  """Initialize TUI context services."""
@@ -753,6 +727,12 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
753
727
  return None
754
728
  return str(self._selected_account.get("name", ""))
755
729
 
730
+ def _show_loading(self, message: str | None = None) -> None:
731
+ show_loading_indicator(self, "#harlequin-loading", message=message, set_status=self._set_status)
732
+
733
+ def _hide_loading(self) -> None:
734
+ hide_loading_indicator(self, "#harlequin-loading")
735
+
756
736
  def action_switch_account(self) -> None:
757
737
  """Switch to the currently selected account."""
758
738
  if self._env_lock:
@@ -806,6 +786,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
806
786
  self._set_status(f"Switch failed: {exc}", "red")
807
787
  return
808
788
  finally:
789
+ self._hide_loading()
809
790
  self._is_switching = False
810
791
 
811
792
  if switched:
@@ -822,8 +803,10 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
822
803
  self._set_status(message or "Switch failed; kept previous account.", "yellow")
823
804
 
824
805
  try:
806
+ self._show_loading(f"Connecting to '{name}'...")
825
807
  self.track_task(perform(), logger=logging.getLogger(__name__))
826
808
  except Exception as exc:
809
+ self._hide_loading()
827
810
  self._is_switching = False
828
811
  self._set_status(f"Switch failed to start: {exc}", "red")
829
812
 
@@ -881,6 +864,15 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
881
864
  self._filter_text = (event.value or "").strip()
882
865
  self._reload_accounts_list()
883
866
 
867
+ def on_input_submitted(self, event: Input.Submitted) -> None:
868
+ """Handle Enter key in Harlequin filter input."""
869
+ if event.input.id == "harlequin-filter":
870
+ try:
871
+ table = self.query_one(HARLEQUIN_ACCOUNTS_LIST_ID, DataTable)
872
+ table.focus()
873
+ except Exception:
874
+ pass
875
+
884
876
  def action_add_account(self) -> None:
885
877
  """Open add account modal."""
886
878
  if self._check_env_lock():
@@ -1144,6 +1136,8 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
1144
1136
  warning=tokens.warning,
1145
1137
  error=tokens.error,
1146
1138
  success=tokens.success,
1139
+ background=tokens.background,
1140
+ surface=tokens.background_panel,
1147
1141
  )
1148
1142
  )
1149
1143
 
@@ -1371,16 +1365,23 @@ class AccountsTextualApp( # pragma: no cover - interactive
1371
1365
 
1372
1366
  def on_input_submitted(self, event: Input.Submitted) -> None:
1373
1367
  """Apply filter when user presses Enter inside filter input."""
1368
+ # Skip if a screen other than the default app screen is active (e.g., Harlequin or Modal)
1369
+ if self.screen.id != "_default":
1370
+ return
1371
+
1374
1372
  self._filter_text = (event.value or "").strip()
1375
1373
  self._reload_rows()
1376
- table = self.query_one(ACCOUNTS_TABLE_ID, DataTable)
1377
- table.focus()
1374
+ try:
1375
+ table = self.query_one(ACCOUNTS_TABLE_ID, DataTable)
1376
+ table.focus()
1377
+ except Exception:
1378
+ pass
1378
1379
  self._update_filter_button_visibility()
1379
1380
 
1380
1381
  def on_input_changed(self, event: Input.Changed) -> None:
1381
1382
  """Apply filter live as the user types."""
1382
- # Skip if Harlequin screen is active (it handles its own filtering)
1383
- if isinstance(self.screen, AccountsHarlequinScreen):
1383
+ # Skip if a screen other than the default app screen is active (e.g., Harlequin or Modal)
1384
+ if self.screen.id != "_default":
1384
1385
  return
1385
1386
  self._filter_text = (event.value or "").strip()
1386
1387
  self._reload_rows()
@@ -0,0 +1,341 @@
1
+ """TUI animated indicators for waiting states."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from rich.text import Text
8
+ from textual._context import NoActiveAppError
9
+ from textual.timer import Timer
10
+ from textual.widgets import Static
11
+
12
+ from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
13
+
14
+ DEFAULT_MESSAGE = "Processing…"
15
+ DEFAULT_WIDTH = 20
16
+ DEFAULT_SPEED_MS = 40
17
+
18
+ BAR_GLYPH = " "
19
+ PULSE_GLYPH = "█"
20
+
21
+ VARIANT_STYLES: dict[str, str] = {
22
+ # Default hex colors matching gl-dark theme (see theme/catalog.py)
23
+ # These are used as fallbacks when the app theme is not active
24
+ "accent": "#C77DFF",
25
+ "primary": "#6EA8FE",
26
+ "success": "#34D399",
27
+ "warning": "#FBBF24",
28
+ "error": "#F87171",
29
+ "info": "#60A5FA",
30
+ "subtle": "#9CA3AF",
31
+ }
32
+
33
+
34
+ class PulseIndicator(Static):
35
+ """A Codex-style moving light/pulse indicator for waiting states.
36
+
37
+ Mirrors the 'Knight Rider' / Cylon scanner animation pattern.
38
+ Specified in specs/architecture/cli-textual-animated-indicators/spec.md
39
+ """
40
+
41
+ DEFAULT_CSS = """
42
+ PulseIndicator {
43
+ width: auto;
44
+ height: 3;
45
+ content-align: center middle;
46
+ padding: 0 2;
47
+ border: round #666666;
48
+ color: $text;
49
+ background: $surface;
50
+ }
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ message: str | None = None,
56
+ *,
57
+ width: int = DEFAULT_WIDTH,
58
+ speed_ms: int = DEFAULT_SPEED_MS,
59
+ variant: str = "accent",
60
+ low_motion: bool = False,
61
+ **kwargs: Any,
62
+ ) -> None:
63
+ """Initialize the PulseIndicator."""
64
+ super().__init__(**kwargs)
65
+ self._width = self._coerce_width(width)
66
+ self._speed_ms = self._coerce_speed(speed_ms)
67
+ self._variant = self._coerce_variant(variant)
68
+ self._message = self._normalize_message(message)
69
+ self._low_motion = bool(low_motion)
70
+ self._position = 0
71
+ self._direction = 1
72
+ self._timer: Timer | None = None
73
+ self._pending_render: Text | None = None
74
+ self.can_focus = False
75
+ self.accessible_label = self._message
76
+
77
+ def on_mount(self) -> None:
78
+ """Handle component mounting."""
79
+ # Initial render happens here to ensure component is ready for updates
80
+ self._safe_update(self._render_static() if self._low_motion else self._render_frame())
81
+ if self._pending_render is not None:
82
+ return
83
+ if self._timer is None and not self._low_motion:
84
+ self._timer = self.set_interval(self._speed_ms / 1000, self._tick)
85
+
86
+ def start(self, message: str | None = None) -> None:
87
+ """Start the pulse animation."""
88
+ if message is not None:
89
+ self.update_message(message)
90
+ self._apply_pending_render()
91
+ self._cancel_timer()
92
+ if self._low_motion:
93
+ self._position = 0
94
+ self._safe_update(self._render_static())
95
+ return
96
+ self._timer = self.set_interval(self._speed_ms / 1000, self._tick)
97
+ self._safe_update(self._render_frame())
98
+
99
+ def stop(self, message: str | None = None) -> None:
100
+ """Stop the pulse animation."""
101
+ if message is not None:
102
+ self.update_message(message)
103
+ self._cancel_timer()
104
+ self._position = 0
105
+ self._direction = 1
106
+ self._safe_update(self._render_static())
107
+
108
+ def update_message(self, message: str) -> None:
109
+ """Update the display message."""
110
+ self._message = self._normalize_message(message)
111
+ self.accessible_label = self._message
112
+ self._safe_update(self._render_static() if self._low_motion else self._render_frame())
113
+
114
+ def _tick(self) -> None:
115
+ self._position += self._direction
116
+ if self._position >= self._width - 1:
117
+ self._position = self._width - 1
118
+ self._direction = -1
119
+ elif self._position <= 0:
120
+ self._position = 0
121
+ self._direction = 1
122
+ self._safe_update(self._render_frame())
123
+
124
+ def _render_frame(self) -> Text:
125
+ bar = self._render_bar(self._position, active=True)
126
+ bar.append(" ")
127
+ bar.append(self._message, style=self._message_style)
128
+ return bar
129
+
130
+ def _render_static(self) -> Text:
131
+ bar = self._render_bar(0, active=False)
132
+ bar.append(" ")
133
+ bar.append(self._message, style=self._message_style)
134
+ return bar
135
+
136
+ def _render_bar(self, position: int, *, active: bool) -> Text:
137
+ bg = self._resolve_style("on #111111", "$surface", is_bg=True)
138
+ bar = Text("[", style=f"grey37 {bg}")
139
+
140
+ p = position
141
+ v = self._active_style
142
+
143
+ for index in range(self._width):
144
+ if not active:
145
+ glyph = "█"
146
+ style = f"dim {v} {bg}"
147
+ else:
148
+ glyph, style = self._get_pulse_glyph_and_style(index, p, v, bg)
149
+
150
+ bar.append(glyph, style=style)
151
+
152
+ bar.append("]", style=f"grey37 {bg}")
153
+ return bar
154
+
155
+ def _get_pulse_glyph_and_style(self, index: int, p: int, v: str, bg: str) -> tuple[str, str]:
156
+ """Determine glyph and style for a bar position during animation."""
157
+ dist = abs(index - p)
158
+ if dist == 0:
159
+ return "█", f"bold white {bg}"
160
+ if dist == 1:
161
+ return "█", f"{v} {bg}"
162
+ if dist == 2:
163
+ return "▓", f"dim {v} {bg}"
164
+ if dist == 3:
165
+ return "▒", f"dim {v} {bg}"
166
+ return " ", bg
167
+
168
+ @property
169
+ def _active_style(self) -> str:
170
+ token = f"${self._variant}"
171
+ fallback = VARIANT_STYLES.get(self._variant, VARIANT_STYLES["accent"])
172
+ return self._resolve_style(fallback, token)
173
+
174
+ @property
175
+ def _message_style(self) -> str:
176
+ token = "$text-muted" if self._variant == "subtle" else "$text"
177
+ fallback = VARIANT_STYLES["subtle"] if self._variant == "subtle" else "white"
178
+ return self._resolve_style(fallback, token)
179
+
180
+ def _resolve_style(self, fallback: str, token: str | None = None, *, is_bg: bool = False) -> str:
181
+ """Resolve a theme token to a Rich style string with fallback."""
182
+ try:
183
+ # Standard resolution sequence
184
+ res = self._do_resolve(token, is_bg)
185
+ if res:
186
+ return res
187
+
188
+ # Specific background resolution fallback
189
+ if is_bg:
190
+ res = self._do_resolve("$surface", True) or self._do_resolve("$background", True)
191
+ if res:
192
+ return res
193
+ except (NoActiveAppError, AttributeError):
194
+ pass
195
+ return fallback
196
+
197
+ def _do_resolve(self, token: str | None, is_bg: bool) -> str | None:
198
+ """Internal resolver that tries multiple sources."""
199
+ if not token:
200
+ return None
201
+
202
+ # 1. Try resolving via component styles
203
+ if token.startswith("$"):
204
+ res = self._resolve_from_component(token, is_bg)
205
+ if res:
206
+ return res
207
+
208
+ # 2. Try direct variable lookup (App.theme_variables or Theme.variables)
209
+ res = self._resolve_from_theme_vars(token.lstrip("$"), is_bg)
210
+ if res:
211
+ return res
212
+
213
+ # 3. Try our built-in theme catalog
214
+ return self._resolve_from_catalog(token.lstrip("$"), is_bg)
215
+
216
+ def _resolve_from_component(self, token: str, is_bg: bool) -> str | None:
217
+ """Resolve style from Textual component registry."""
218
+ try:
219
+ style = self.app.get_component_rich_style(token)
220
+ color = style.bgcolor if is_bg else style.color
221
+ if color:
222
+ return self._color_to_rich_style(color, is_bg)
223
+ except Exception:
224
+ pass
225
+ return None
226
+
227
+ def _resolve_from_theme_vars(self, var_name: str, is_bg: bool) -> str | None:
228
+ """Resolve color from theme variables dictionary."""
229
+ try:
230
+ app = self.app
231
+ # Check theme_variables first
232
+ val = getattr(app, "theme_variables", {}).get(var_name)
233
+ if val is None:
234
+ # Fallback to current theme object's variables (Textual 0.52+)
235
+ theme_obj = app.get_theme(app.theme)
236
+ if theme_obj and hasattr(theme_obj, "variables"):
237
+ val = theme_obj.variables.get(var_name)
238
+
239
+ if val:
240
+ return self._color_to_rich_style(val, is_bg)
241
+ except Exception:
242
+ pass
243
+ return None
244
+
245
+ def _resolve_from_catalog(self, var_name: str, is_bg: bool) -> str | None:
246
+ """Resolve color from our built-in theme catalog."""
247
+ try:
248
+ theme_name = getattr(self.app, "theme", "gl-dark")
249
+ theme_tokens = _BUILTIN_THEMES.get(theme_name, _BUILTIN_THEMES["gl-dark"])
250
+ val = getattr(theme_tokens, var_name.replace("-", "_"), None)
251
+ if val:
252
+ return self._color_to_rich_style(val, is_bg)
253
+ except Exception:
254
+ pass
255
+ return None
256
+
257
+ def _color_to_rich_style(self, color: Any, is_bg: bool) -> str | None:
258
+ """Convert any color-like object to a Rich-compatible style string."""
259
+ if not color:
260
+ return None
261
+
262
+ # 1. Textual Color objects
263
+ if hasattr(color, "hex") and color.hex.startswith("#"):
264
+ return f"on {color.hex}" if is_bg else color.hex
265
+
266
+ # 2. Rich Color objects (with triplets)
267
+ if hasattr(color, "triplet") and color.triplet:
268
+ hex_val = color.triplet.hex
269
+ return f"on {hex_val}" if is_bg else hex_val
270
+
271
+ # 3. Strings or named colors
272
+ return self._str_color_to_style(color, is_bg)
273
+
274
+ def _str_color_to_style(self, color: Any, is_bg: bool) -> str | None:
275
+ """Helper to convert string-based colors to style."""
276
+ if color is None:
277
+ return None
278
+ c_str = str(color).strip()
279
+ if not c_str:
280
+ return None
281
+
282
+ if c_str.startswith("#"):
283
+ return f"on {c_str}" if is_bg else c_str
284
+
285
+ # If it's a named color like 'white', Rich understands it directly
286
+ # but we skip Textual's 'color(N)' internal format.
287
+ if not c_str.startswith("color(") and not c_str.startswith("auto"):
288
+ return f"on {c_str}" if is_bg else c_str
289
+
290
+ return None
291
+
292
+ def _safe_update(self, renderable: Text) -> None:
293
+ try:
294
+ self.update(renderable)
295
+ self._pending_render = None
296
+ except NoActiveAppError:
297
+ self._pending_render = renderable
298
+
299
+ def _apply_pending_render(self) -> None:
300
+ if self._pending_render is None:
301
+ return
302
+ try:
303
+ self.update(self._pending_render)
304
+ self._pending_render = None
305
+ except NoActiveAppError:
306
+ return
307
+
308
+ def _cancel_timer(self) -> None:
309
+ if self._timer is None:
310
+ return
311
+ try:
312
+ self._timer.stop()
313
+ except Exception:
314
+ pass
315
+ self._timer = None
316
+
317
+ @staticmethod
318
+ def _normalize_message(message: str | None) -> str:
319
+ if message is None:
320
+ return DEFAULT_MESSAGE
321
+ cleaned = str(message).strip()
322
+ return cleaned if cleaned else DEFAULT_MESSAGE
323
+
324
+ @staticmethod
325
+ def _coerce_width(width: int) -> int:
326
+ if not isinstance(width, int):
327
+ return DEFAULT_WIDTH
328
+ return width if width > 0 else DEFAULT_WIDTH
329
+
330
+ @staticmethod
331
+ def _coerce_speed(speed_ms: int) -> int:
332
+ if not isinstance(speed_ms, int):
333
+ return DEFAULT_SPEED_MS
334
+ return speed_ms if speed_ms > 0 else DEFAULT_SPEED_MS
335
+
336
+ @staticmethod
337
+ def _coerce_variant(variant: str) -> str:
338
+ if not isinstance(variant, str):
339
+ return "accent"
340
+ normalized = variant.strip().lower()
341
+ return normalized if normalized in VARIANT_STYLES else "accent"
@@ -1,58 +1,80 @@
1
1
  """Shared helpers for toggling Textual loading indicators.
2
2
 
3
- Note: uses Textual's built-in LoadingIndicator as the MVP; upgrade to the
4
- PulseIndicator from cli-textual-animated-indicators.md when shipped.
3
+ This module provides unified helpers for showing/hiding both the built-in
4
+ Textual LoadingIndicator and the custom PulseIndicator.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
9
  from collections.abc import Callable
10
- from typing import TYPE_CHECKING
10
+ from typing import Any
11
11
 
12
- try: # pragma: no cover - optional dependency
13
- from textual.widgets import LoadingIndicator
14
- except Exception: # pragma: no cover - optional dependency
15
- LoadingIndicator = None # type: ignore[assignment]
12
+ from textual.widgets import LoadingIndicator
16
13
 
17
- if TYPE_CHECKING: # pragma: no cover - type checking aid
18
- from textual.widgets import LoadingIndicator as _LoadingIndicatorType
14
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
19
15
 
20
- LoadingIndicator: type[_LoadingIndicatorType] | None
21
16
 
22
-
23
- def _set_indicator_display(app: object, selector: str, visible: bool) -> None:
24
- """Safely toggle a LoadingIndicator's display property."""
25
- if LoadingIndicator is None:
17
+ def _set_indicator_display(app: Any, selector: str, visible: bool) -> None:
18
+ try:
19
+ indicator = app.query_one(selector, PulseIndicator)
20
+ if visible:
21
+ indicator.display = True
22
+ indicator.start()
23
+ else:
24
+ indicator.stop()
25
+ indicator.display = False
26
26
  return
27
+ except Exception:
28
+ pass
29
+
27
30
  try:
28
- indicator = app.query_one(selector, LoadingIndicator) # type: ignore[arg-type]
31
+ indicator = app.query_one(selector, LoadingIndicator)
29
32
  indicator.display = visible
30
33
  except Exception:
31
- # Ignore lookup/rendering errors to keep UI resilient
32
34
  return
33
35
 
34
36
 
35
37
  def show_loading_indicator(
36
- app: object,
38
+ app: Any,
37
39
  selector: str,
38
40
  *,
39
41
  message: str | None = None,
40
42
  set_status: Callable[..., None] | None = None,
41
43
  status_style: str = "cyan",
42
44
  ) -> None:
43
- """Show a loading indicator and optionally set a status message."""
45
+ """Show a loading indicator (PulseIndicator or LoadingIndicator) and optionally set a status message.
46
+
47
+ Args:
48
+ app: Textual app instance containing the indicator widget
49
+ selector: CSS selector for the indicator widget
50
+ message: Optional message to display in the indicator
51
+ set_status: Optional callback to set status message (for fallback display)
52
+ status_style: Style for status message if set_status is provided
53
+ """
44
54
  _set_indicator_display(app, selector, True)
55
+
56
+ try:
57
+ indicator = app.query_one(selector, PulseIndicator)
58
+ if message:
59
+ indicator.update_message(message)
60
+ except Exception:
61
+ pass
62
+
45
63
  if message and set_status:
46
64
  try:
47
65
  set_status(message, status_style)
48
66
  except TypeError:
49
- # Fallback for setters that accept only a single arg or kwargs
50
67
  try:
51
68
  set_status(message)
52
69
  except Exception:
53
70
  return
54
71
 
55
72
 
56
- def hide_loading_indicator(app: object, selector: str) -> None:
57
- """Hide a loading indicator."""
73
+ def hide_loading_indicator(app: Any, selector: str) -> None:
74
+ """Hide a loading indicator (PulseIndicator or LoadingIndicator).
75
+
76
+ Args:
77
+ app: Textual app instance containing the indicator widget
78
+ selector: CSS selector for the indicator widget
79
+ """
58
80
  _set_indicator_display(app, selector, False)
@@ -18,32 +18,17 @@ from typing import Any
18
18
 
19
19
  from rich.text import Text
20
20
 
21
- try: # pragma: no cover - optional dependency
22
- from textual.app import App, ComposeResult
23
- from textual.binding import Binding
24
- from textual.containers import Horizontal, Vertical
25
- from textual.coordinate import Coordinate
26
- from textual.reactive import ReactiveError
27
- from textual.screen import ModalScreen
28
- from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
29
- except Exception: # pragma: no cover - optional dependency
30
- App = None # type: ignore[assignment]
31
- ComposeResult = None # type: ignore[assignment]
32
- Binding = None # type: ignore[assignment]
33
- Horizontal = None # type: ignore[assignment]
34
- Vertical = None # type: ignore[assignment]
35
- Coordinate = None # type: ignore[assignment]
36
- ReactiveError = Exception # type: ignore[assignment, misc]
37
- ModalScreen = object # type: ignore[assignment, misc]
38
- DataTable = None # type: ignore[assignment]
39
- Footer = None # type: ignore[assignment]
40
- Header = None # type: ignore[assignment]
41
- LoadingIndicator = None # type: ignore[assignment]
42
- RichLog = None # type: ignore[assignment]
43
- Static = None # type: ignore[assignment]
21
+ from textual.app import App, ComposeResult
22
+ from textual.binding import Binding
23
+ from textual.containers import Horizontal, Vertical
24
+ from textual.coordinate import Coordinate
25
+ from textual.reactive import ReactiveError
26
+ from textual.screen import ModalScreen
27
+ from textual.widgets import DataTable, Footer, Header, RichLog, Static
44
28
 
45
29
  from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
46
30
  from glaip_sdk.cli.slash.tui.context import TUIContext
31
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
47
32
  from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
48
33
  from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastContainer, ToastHandlerMixin
49
34
 
@@ -327,7 +312,6 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
327
312
  """Textual application for browsing remote runs."""
328
313
 
329
314
  CSS = f"""
330
- Screen {{ layout: vertical; layers: base toasts; }}
331
315
  #toast-container {{
332
316
  width: 100%;
333
317
  height: auto;
@@ -335,10 +319,14 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
335
319
  align: right top;
336
320
  layer: toasts;
337
321
  }}
338
- #status-bar {{ height: 3; padding: 0 1; }}
339
- #agent-context {{ min-width: 25; padding-right: 1; }}
340
- #{RUNS_LOADING_ID} {{ width: 8; }}
341
- #status {{ padding-left: 1; }}
322
+ #{RUNS_LOADING_ID} {{
323
+ width: auto;
324
+ display: none;
325
+ }}
326
+ #status-bar {{
327
+ height: 3;
328
+ padding: 0 1;
329
+ }}
342
330
  """
343
331
 
344
332
  BINDINGS = [
@@ -400,7 +388,7 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
400
388
  )
401
389
  yield table # pragma: no cover - interactive UI, tested via integration
402
390
  yield Horizontal( # pragma: no cover - interactive UI, tested via integration
403
- LoadingIndicator(id=RUNS_LOADING_ID),
391
+ PulseIndicator(id=RUNS_LOADING_ID),
404
392
  Static(id="status"),
405
393
  id="status-bar",
406
394
  )
@@ -557,7 +545,7 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
557
545
  self._update_status("Already loading run detail. Please wait…", append=True)
558
546
  return
559
547
  run_id = str(run.id)
560
- self._show_loading("Loading run detail…", table_spinner=False)
548
+ self._show_loading("Loading run detail…", table_spinner=False, footer_message=False)
561
549
  self._queue_detail_load(run_id)
562
550
 
563
551
  async def action_export_run(self) -> None:
@@ -750,7 +738,7 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
750
738
  show_loading_indicator(
751
739
  self,
752
740
  RUNS_LOADING_SELECTOR,
753
- message=message if footer_message else None,
741
+ message=message,
754
742
  set_status=self._update_status if footer_message else None,
755
743
  )
756
744
  self._set_table_loading(table_spinner)
glaip_sdk/runner/deps.py CHANGED
@@ -31,7 +31,10 @@ def _probe_aip_agents_import() -> bool:
31
31
  Returns:
32
32
  True if aip_agents appears importable, False otherwise.
33
33
  """
34
- return importlib.util.find_spec("aip_agents") is not None
34
+ try:
35
+ return importlib.util.find_spec("aip_agents") is not None
36
+ except (ImportError, ValueError):
37
+ return False
35
38
 
36
39
 
37
40
  def check_local_runtime_available() -> bool:
@@ -70,6 +70,10 @@ def _swallow_aip_logs(level: int = logging.ERROR) -> None:
70
70
  logger = LoggerManager().get_logger(__name__)
71
71
 
72
72
 
73
+ # Constants for MCP configuration validation
74
+ _MCP_TRANSPORT_KEYS = {"url", "command", "args", "env", "timeout", "headers"}
75
+
76
+
73
77
  def _convert_chat_history_to_messages(
74
78
  chat_history: list[dict[str, str]] | None,
75
79
  ) -> list[BaseMessage]:
@@ -870,39 +874,126 @@ class LangGraphRunner(BaseRunner):
870
874
  base_config: dict[str, Any],
871
875
  override: dict[str, Any] | None,
872
876
  ) -> dict[str, Any]:
873
- """Merge a single MCP config with runtime override.
877
+ """Merge a single MCP config with a runtime override, handling normalization and parity fixes.
878
+
879
+ This method orchestrates the merging of base MCP settings (from the object definition)
880
+ with runtime overrides. It enforces Platform parity by prioritizing the nested 'config'
881
+ block while maintaining robustness for local development by auto-fixing flat transport keys.
882
+
883
+ The merge follows these priority rules (highest to lowest):
884
+ 1. Misplaced flat keys in the override (e.g., 'url' at top level) - Auto-fixed with warning.
885
+ 2. Nested 'config' block in the override (Matches Platform/Constructor schema).
886
+ 3. Authentication objects in the override (Converted to HTTP headers).
887
+ 4. Structural settings in the override (e.g., 'allowed_tools').
888
+ 5. Base configuration from the MCP object definition.
889
+
890
+ Examples:
891
+ >>> # 1. Strict Nested Style (Recommended)
892
+ >>> override = {"config": {"url": "https://new.api"}, "allowed_tools": ["t1"]}
893
+ >>> self._merge_single_mcp_config("mcp", base, override)
894
+ >>> # Result: {"url": "https://new.api", "allowed_tools": ["t1"], ...}
895
+
896
+ >>> # 2. Flat Legacy Style (Auto-fixed with warning)
897
+ >>> override = {"url": "https://new.api"}
898
+ >>> self._merge_single_mcp_config("mcp", base, override)
899
+ >>> # Result: {"url": "https://new.api", ...}
900
+
901
+ >>> # 3. Header Merging (Preserves Auth)
902
+ >>> base = {"headers": {"Authorization": "Bearer token"}}
903
+ >>> override = {"headers": {"X-Custom": "val"}}
904
+ >>> self._merge_single_mcp_config("mcp", base, override)
905
+ >>> # Result: {"headers": {"Authorization": "Bearer token", "X-Custom": "val"}, ...}
874
906
 
875
907
  Args:
876
- server_name: Name of the MCP server.
877
- base_config: Base config from adapter.
878
- override: Optional runtime override config.
908
+ server_name: Name of the MCP server being configured.
909
+ base_config: Base configuration dictionary derived from the MCP object.
910
+ override: Optional dictionary of runtime overrides.
879
911
 
880
912
  Returns:
881
- Merged config dict.
913
+ A fully merged and normalized configuration dictionary ready for the local runner.
882
914
  """
883
915
  merged = base_config.copy()
884
916
 
885
917
  if not override:
886
918
  return merged
887
919
 
888
- from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
889
- MCPConfigBuilder,
890
- )
920
+ # 1. Check for misplaced keys and warn (DX/Parity guidance)
921
+ self._warn_if_mcp_override_misplaced(server_name, override)
922
+
923
+ # 2. Apply Authentication (Converted to headers)
924
+ self._apply_mcp_auth_override(server_name, merged, override)
925
+
926
+ # 3. Apply Transport Settings (Nested 'config')
927
+ if "config" in override and isinstance(override["config"], dict):
928
+ merged.update(override["config"])
891
929
 
892
- # Handle authentication override
893
- if "authentication" in override:
894
- headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
895
- if headers:
896
- merged["headers"] = headers
897
- logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
930
+ # 4. Apply Structural Settings (e.g., allowed_tools)
931
+ if "allowed_tools" in override:
932
+ merged["allowed_tools"] = override["allowed_tools"]
898
933
 
899
- # Merge other config keys (excluding authentication since we converted it)
934
+ # 5. Preserve unknown top-level keys (backward compatibility)
935
+ known_keys = _MCP_TRANSPORT_KEYS | {"config", "authentication", "allowed_tools"}
900
936
  for key, value in override.items():
901
- if key != "authentication":
937
+ if key not in known_keys:
902
938
  merged[key] = value
903
939
 
940
+ # 6. Apply Auto-fix for misplaced keys (Local Success)
941
+ for key in [k for k in override if k in _MCP_TRANSPORT_KEYS]:
942
+ val = override[key]
943
+ # Special case: Merge headers instead of overwriting to preserve auth
944
+ if key == "headers" and isinstance(val, dict) and isinstance(merged.get("headers"), dict):
945
+ merged["headers"].update(val)
946
+ else:
947
+ merged[key] = val
948
+
904
949
  return merged
905
950
 
951
+ def _warn_if_mcp_override_misplaced(self, server_name: str, override: dict[str, Any]) -> None:
952
+ """Log a warning if transport keys are found at the top level of an override.
953
+
954
+ Args:
955
+ server_name: Name of the MCP server.
956
+ override: The raw override dictionary.
957
+ """
958
+ misplaced = [k for k in override if k in _MCP_TRANSPORT_KEYS]
959
+ if misplaced:
960
+ logger.warning(
961
+ "MCP '%s' override contains transport keys at the top level: %s. "
962
+ "This structure is inconsistent with the Platform and MCP constructor. "
963
+ "Transport settings should be nested within a 'config' dictionary. "
964
+ "Example: mcp_configs={'%s': {'config': {'%s': '...'}}}. "
965
+ "Automatically merging top-level keys for local execution parity.",
966
+ server_name,
967
+ misplaced,
968
+ server_name,
969
+ misplaced[0],
970
+ )
971
+
972
+ def _apply_mcp_auth_override(
973
+ self,
974
+ server_name: str,
975
+ merged_config: dict[str, Any],
976
+ override: dict[str, Any],
977
+ ) -> None:
978
+ """Convert authentication override to headers and apply to config.
979
+
980
+ Args:
981
+ server_name: Name of the MCP server.
982
+ merged_config: The configuration being built (mutated in place).
983
+ override: The raw override dictionary.
984
+ """
985
+ if "authentication" not in override:
986
+ return
987
+
988
+ from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
989
+ MCPConfigBuilder,
990
+ )
991
+
992
+ headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
993
+ if headers:
994
+ merged_config["headers"] = headers
995
+ logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
996
+
906
997
  def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
907
998
  """Validate that a sub-agent reference is supported for local execution.
908
999
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glaip-sdk
3
- Version: 0.7.14
3
+ Version: 0.7.16
4
4
  Summary: Python SDK and CLI for GL AIP (GDP Labs AI Agent Package) - Build, run, and manage AI agents
5
5
  Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
6
6
  License: MIT
@@ -5,7 +5,7 @@ glaip_sdk/exceptions.py,sha256=iAChFClkytXRBLP0vZq1_YjoZxA9i4m4bW1gDLiGR1g,2321
5
5
  glaip_sdk/icons.py,sha256=J5THz0ReAmDwIiIooh1_G3Le-mwTJyEjhJDdJ13KRxM,524
6
6
  glaip_sdk/rich_components.py,sha256=44Z0V1ZQleVh9gUDGwRR5mriiYFnVGOhm7fFxZYbP8c,4052
7
7
  glaip_sdk/agents/__init__.py,sha256=VfYov56edbWuySXFEbWJ_jLXgwnFzPk1KB-9-mfsUCc,776
8
- glaip_sdk/agents/base.py,sha256=Aq7bDGHAw7214wW-9Re_uAzbaGphIUpIIbuibuDf7i4,49131
8
+ glaip_sdk/agents/base.py,sha256=OCJP2yBOo1rYqUAzpwdcEb2ZjIGHj9-ivDvldzfQX48,50813
9
9
  glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
10
10
  glaip_sdk/cli/account_store.py,sha256=u_memecwEQssustZs2wYBrHbEmKUlDfmmL-zO1F3n3A,19034
11
11
  glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
@@ -78,16 +78,17 @@ glaip_sdk/cli/slash/accounts_shared.py,sha256=Mq5HxlI0YsVEQ0KKISWvyBZhzOFFWCzwRb
78
78
  glaip_sdk/cli/slash/agent_session.py,sha256=tuVOme-NbEyr6rwJvsBEKZYWQmsaRf4piJeRvIGu0ns,11384
79
79
  glaip_sdk/cli/slash/prompt.py,sha256=q4f1c2zr7ZMUeO6AgOBF2Nz4qgMOXrVPt6WzPRQMbAM,8501
80
80
  glaip_sdk/cli/slash/remote_runs_controller.py,sha256=iLl4a-mu9QU7dcedgEILewPtDIVtFUJkbKGtcx1F66U,21445
81
- glaip_sdk/cli/slash/session.py,sha256=YJ7UIeWyged1znmBVnGweOzH2l4NKeF5lT9VGdDvQWo,75998
82
- glaip_sdk/cli/slash/tui/__init__.py,sha256=oBUzeoslYwPKVlhqhgg4I7480b77vQNc9ec0NgdTC1s,977
83
- glaip_sdk/cli/slash/tui/accounts.tcss,sha256=GA5NFu_VSPRQdpfTaO0iUHjGDh8U0QDYqiI-WY59ZTc,2356
84
- glaip_sdk/cli/slash/tui/accounts_app.py,sha256=mv4SzBZrAvS-q-_kMEQQbQStiJPaJ71AmvcaoXiQm0A,73518
81
+ glaip_sdk/cli/slash/session.py,sha256=jWTPrt374tDTt3tN-nBQ5wb2ssc60yMSAcSp4FNej2Y,76308
82
+ glaip_sdk/cli/slash/tui/__init__.py,sha256=hAjH4ULBhRpQzA6fBLWRV6LiVm8UM3lgPurjoX9muYU,1061
83
+ glaip_sdk/cli/slash/tui/accounts.tcss,sha256=5iVZZfS10CTJhnoZ9AFJejtj8nyQXH9xV7u9k8jSkGE,2411
84
+ glaip_sdk/cli/slash/tui/accounts_app.py,sha256=5FtQy57xdUtBfOVRsqjXSbWLsLna7wIoMOz9t3h_ptQ,73187
85
85
  glaip_sdk/cli/slash/tui/background_tasks.py,sha256=SAe1mV2vXB3mJcSGhelU950vf8Lifjhws9iomyIVFKw,2422
86
86
  glaip_sdk/cli/slash/tui/clipboard.py,sha256=7fEshhTwHYaj-n7n0W0AsWTs8W0RLZw_9luXxrFTrtw,6227
87
87
  glaip_sdk/cli/slash/tui/context.py,sha256=mzI4TDXnfZd42osACp5uo10d10y1_A0z6IxRK1KVoVk,3320
88
+ glaip_sdk/cli/slash/tui/indicators.py,sha256=jV3fFvEVWQ0inWJJ-B1fMsdkF0Uq2zwX3xcl0YWPHSE,11768
88
89
  glaip_sdk/cli/slash/tui/keybind_registry.py,sha256=_rK05BxTxNudYc4iJ9gDxpgeUkjDAq8rarIT-9A-jyM,6739
89
- glaip_sdk/cli/slash/tui/loading.py,sha256=nW5pv_Tnl9FUOPR3Qf2O5gt1AGHSo3b5-Uofg34F6AE,1909
90
- glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=F50pn2VmCN6RQImhQjNZvpE4gU3eUj6dV32_w1Gg5JU,30282
90
+ glaip_sdk/cli/slash/tui/loading.py,sha256=Ku7HyQ_h-r2dJQ5aIEaCOi5PUu5gSsYle8oiKHIxfKI,2336
91
+ glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=MVbzx-VJGi-wGSzr_CjQbdZbcWboVEbrCZUCuH2AeJM,29388
91
92
  glaip_sdk/cli/slash/tui/terminal.py,sha256=ZAC3sB17TGpl-GFeRVm_nI8DQTN3pyti3ynlZ41wT_A,12323
92
93
  glaip_sdk/cli/slash/tui/toast.py,sha256=XGITLHhO40xIGmtg9hanPmDsPCQY2hQXzoM_9mJXQyg,12442
93
94
  glaip_sdk/cli/slash/tui/layouts/__init__.py,sha256=KT77pZHa7Wz84QlHYT2mfhQ_AXUA-T0eHv_HtAvc1ac,473
@@ -150,8 +151,8 @@ glaip_sdk/registry/mcp.py,sha256=kNJmiijIbZL9Btx5o2tFtbaT-WG6O4Xf_nl3wz356Ow,797
150
151
  glaip_sdk/registry/tool.py,sha256=c0Ja4rFYMOKs_1yjDLDZxCId4IjQzprwXzX0iIL8Fio,14979
151
152
  glaip_sdk/runner/__init__.py,sha256=orJ3nLR9P-n1qMaAMWZ_xRS4368YnDpdltg-bX5BlUk,2210
152
153
  glaip_sdk/runner/base.py,sha256=KIjcSAyDCP9_mn2H4rXR5gu1FZlwD9pe0gkTBmr6Yi4,2663
153
- glaip_sdk/runner/deps.py,sha256=Du3hr2R5RHOYCRAv7RVmx661x-ayVXIeZ8JD7ODirTA,3884
154
- glaip_sdk/runner/langgraph.py,sha256=sKF8JMMRicr9iiDNA8hIoZVfvfhjT_TMqlYfvW3yT6w,36379
154
+ glaip_sdk/runner/deps.py,sha256=Lv8LdIF6H4JGzzvLmi-MgG72RJYgB-MsQNRx8yY7cl4,3956
155
+ glaip_sdk/runner/langgraph.py,sha256=HB1n6w44LdyE7OSB2iSEtwhm_IeD7fGi_20erCZurX4,40869
155
156
  glaip_sdk/runner/logging_config.py,sha256=OrQgW23t42qQRqEXKH8U4bFg4JG5EEkUJTlbvtU65iE,2528
156
157
  glaip_sdk/runner/mcp_adapter/__init__.py,sha256=Rdttfg3N6kg3-DaTCKqaGXKByZyBt0Mwf6FV8s_5kI8,462
157
158
  glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py,sha256=ic56fKgb3zgVZZQm3ClWUZi7pE1t4EVq8mOg6AM6hdA,1374
@@ -216,8 +217,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
216
217
  glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
217
218
  glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
218
219
  glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
219
- glaip_sdk-0.7.14.dist-info/METADATA,sha256=zUdx8wiJbp1JiDNlntuItt5_Y1krwQRZDhpyEcnGcDo,8528
220
- glaip_sdk-0.7.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
221
- glaip_sdk-0.7.14.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
222
- glaip_sdk-0.7.14.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
223
- glaip_sdk-0.7.14.dist-info/RECORD,,
220
+ glaip_sdk-0.7.16.dist-info/METADATA,sha256=oqaUXPHF7LzD6CCvuJoHe10WD8OMhzMlKOKSgqwpx-E,8528
221
+ glaip_sdk-0.7.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
222
+ glaip_sdk-0.7.16.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
223
+ glaip_sdk-0.7.16.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
224
+ glaip_sdk-0.7.16.dist-info/RECORD,,