glaip-sdk 0.7.12__py3-none-any.whl → 0.7.14__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
@@ -54,10 +54,13 @@ from glaip_sdk.utils.resource_refs import is_uuid
54
54
 
55
55
  if TYPE_CHECKING:
56
56
  from glaip_sdk.client.schedules import AgentScheduleManager
57
+ from glaip_sdk.models import AgentResponse, Model
57
58
  from glaip_sdk.guardrails import GuardrailManager
58
- from glaip_sdk.models import AgentResponse
59
59
  from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
60
60
 
61
+ # Import model validation utility
62
+ from glaip_sdk.models._validation import _validate_model
63
+
61
64
  logger = logging.getLogger(__name__)
62
65
 
63
66
  _AGENT_NOT_DEPLOYED_MSG = "Agent must be deployed before running. Call deploy() first."
@@ -99,11 +102,11 @@ class Agent:
99
102
  - instruction: Agent instruction text (required)
100
103
  - description: Agent description (default: "")
101
104
  - tools: List of tools (default: [])
105
+ - model: Optional model override (default: None)
102
106
  - agents: List of sub-agents (default: [])
103
107
  - mcps: List of MCPs (default: [])
104
108
  - timeout: Timeout in seconds (default: 300)
105
109
  - metadata: Optional metadata dict (default: None)
106
- - model: Optional model override (default: None)
107
110
  - framework: Agent framework (default: "langchain")
108
111
  - version: Agent version (default: "1.0.0")
109
112
  - agent_type: Agent type (default: "config")
@@ -130,7 +133,7 @@ class Agent:
130
133
  tools: list | None = None,
131
134
  agents: list | None = None,
132
135
  mcps: list | None = None,
133
- model: str | None = _UNSET, # type: ignore[assignment]
136
+ model: str | Model | None = _UNSET, # type: ignore[assignment]
134
137
  guardrail: GuardrailManager | None = None,
135
138
  _client: Any = None,
136
139
  **kwargs: Any,
@@ -148,7 +151,7 @@ class Agent:
148
151
  tools: List of tools (Tool classes, SDK Tool objects, or strings).
149
152
  agents: List of sub-agents (Agent classes, instances, or strings).
150
153
  mcps: List of MCPs.
151
- model: Model identifier.
154
+ model: Model identifier or Model configuration object.
152
155
  guardrail: The guardrail manager for content safety.
153
156
  _client: Internal client reference (set automatically).
154
157
 
@@ -176,7 +179,7 @@ class Agent:
176
179
  self._tools = tools
177
180
  self._agents = agents
178
181
  self._mcps = mcps
179
- self._model = model
182
+ self._model = self._validate_and_set_model(model)
180
183
  self._guardrail = guardrail
181
184
  self._language_model_id: str | None = None
182
185
  # Extract parameters from kwargs with _UNSET defaults
@@ -185,6 +188,18 @@ class Agent:
185
188
  self._framework = kwargs.pop("framework", Agent._UNSET) # type: ignore[assignment]
186
189
  self._version = kwargs.pop("version", Agent._UNSET) # type: ignore[assignment]
187
190
  self._agent_type = kwargs.pop("agent_type", Agent._UNSET) # type: ignore[assignment]
191
+
192
+ # Handle 'type' as a legacy alias for 'agent_type'
193
+ legacy_type = kwargs.pop("type", Agent._UNSET)
194
+ if legacy_type is not Agent._UNSET:
195
+ warnings.warn(
196
+ "The 'type' parameter is deprecated and will be removed in a future version. Use 'agent_type' instead.",
197
+ DeprecationWarning,
198
+ stacklevel=2,
199
+ )
200
+ if self._agent_type is Agent._UNSET:
201
+ self._agent_type = legacy_type
202
+
188
203
  self._agent_config = kwargs.pop("agent_config", Agent._UNSET) # type: ignore[assignment]
189
204
  self._tool_configs = kwargs.pop("tool_configs", Agent._UNSET) # type: ignore[assignment]
190
205
  self._mcp_configs = kwargs.pop("mcp_configs", Agent._UNSET) # type: ignore[assignment]
@@ -198,6 +213,30 @@ class Agent:
198
213
  stacklevel=2,
199
214
  )
200
215
 
216
+ def _validate_and_set_model(self, model: str | Any) -> str | Any:
217
+ """Validate and normalize model parameter.
218
+
219
+ Supports both string model identifiers and Model objects:
220
+ - String: Simple model identifier (e.g., "openai/gpt-4o" or OpenAI.GPT_4O)
221
+ - Model: Model object with credentials/hyperparameters for local execution
222
+
223
+ Args:
224
+ model: Model identifier (string) or Model object.
225
+
226
+ Returns:
227
+ Validated model (string or Model object).
228
+ """
229
+ if model is None or model is Agent._UNSET:
230
+ return model
231
+
232
+ from glaip_sdk.models import Model # noqa: PLC0415
233
+
234
+ if isinstance(model, str):
235
+ return _validate_model(model)
236
+ elif isinstance(model, Model):
237
+ return model
238
+ return model
239
+
201
240
  # ─────────────────────────────────────────────────────────────────
202
241
  # Properties (override in subclasses OR pass to __init__)
203
242
  # ─────────────────────────────────────────────────────────────────
@@ -342,11 +381,11 @@ class Agent:
342
381
  return None
343
382
 
344
383
  @property
345
- def model(self) -> str | None:
384
+ def model(self) -> str | Model | None:
346
385
  """Optional model override.
347
386
 
348
387
  Returns:
349
- Model identifier string or None to use default.
388
+ Model identifier string, Model object, or None to use default.
350
389
  """
351
390
  if self._model is not self._UNSET:
352
391
  return self._model
@@ -549,9 +588,14 @@ class Agent:
549
588
  "framework": self.framework,
550
589
  "version": self.version,
551
590
  "agent_type": self.agent_type,
552
- "model": self.model,
553
591
  }
554
592
 
593
+ if self.model:
594
+ if isinstance(self.model, str):
595
+ config["model"] = self.model
596
+ else:
597
+ config["model"] = self.model.id
598
+
555
599
  # Handle metadata (default to empty dict if None)
556
600
  config["metadata"] = self.metadata or {}
557
601
 
@@ -848,21 +892,42 @@ class Agent:
848
892
  """Return a dict representation of the Agent.
849
893
 
850
894
  Provides Pydantic-style serialization for backward compatibility.
895
+ This implementation avoids triggering external tool resolution to ensure
896
+ it remains robust even when the environment is not fully configured.
851
897
 
852
898
  Args:
853
899
  exclude_none: If True, exclude None values from the output.
854
900
 
855
901
  Returns:
856
- Dictionary containing Agent attributes.
902
+ Dictionary containing Agent attributes. Note: Mutable fields (dicts, lists)
903
+ are returned as references. Modify with caution or make a deep copy if needed.
857
904
  """
858
- config = self._build_config(get_tool_registry(), get_mcp_registry())
905
+ # Map convenience timeout to agent_config if not already present
906
+ agent_config = self.agent_config if self.agent_config is not self._UNSET else {}
907
+ agent_config = dict(agent_config) if agent_config else {}
908
+
909
+ if self.timeout and "execution_timeout" not in agent_config:
910
+ agent_config["execution_timeout"] = self.timeout
911
+
912
+ # Handle guardrail serialization without full config build
913
+ if self.guardrail:
914
+ try:
915
+ from glaip_sdk.guardrails.serializer import ( # noqa: PLC0415
916
+ serialize_guardrail_manager,
917
+ )
918
+
919
+ agent_config["guardrails"] = serialize_guardrail_manager(self.guardrail)
920
+ except ImportError: # pragma: no cover
921
+ # Serializer not available (optional dependency); skip guardrail data
922
+ pass
859
923
 
860
924
  data = {
861
925
  "id": self._id,
862
926
  "name": self.name,
863
927
  "instruction": self.instruction,
864
928
  "description": self.description,
865
- "type": self.agent_type,
929
+ "agent_type": self.agent_type,
930
+ "type": self.agent_type, # Legacy key for backward compatibility
866
931
  "framework": self.framework,
867
932
  "version": self.version,
868
933
  "tools": self.tools,
@@ -870,7 +935,8 @@ class Agent:
870
935
  "mcps": self.mcps,
871
936
  "timeout": self.timeout,
872
937
  "metadata": self.metadata,
873
- "agent_config": config.get("agent_config"),
938
+ "model": self.model,
939
+ "agent_config": agent_config,
874
940
  "tool_configs": self.tool_configs,
875
941
  "mcp_configs": self.mcp_configs,
876
942
  "a2a_profile": self.a2a_profile,
@@ -878,6 +944,7 @@ class Agent:
878
944
  "created_at": self._created_at,
879
945
  "updated_at": self._updated_at,
880
946
  }
947
+
881
948
  if exclude_none:
882
949
  return {k: v for k, v in data.items() if v is not None}
883
950
  return data
@@ -11,37 +11,30 @@ Authors:
11
11
  # Import from submodules
12
12
  from glaip_sdk.cli.commands.agents._common import ( # noqa: E402
13
13
  AGENT_NOT_FOUND_ERROR,
14
- _get_agent_for_update,
15
- _resolve_agent,
16
- agents_group,
17
14
  _coerce_mapping_candidate,
18
15
  _display_agent_details,
19
16
  _emit_verbose_guidance,
20
17
  _fetch_full_agent_details,
18
+ _get_agent_for_update,
21
19
  _get_agent_model_name,
22
20
  _get_language_model_display_name,
23
21
  _model_from_config,
24
22
  _prepare_agent_output,
23
+ _resolve_agent,
25
24
  _resolve_resources_by_name,
25
+ agents_group,
26
26
  console,
27
27
  )
28
28
  from glaip_sdk.cli.commands.agents.create import create # noqa: E402
29
29
  from glaip_sdk.cli.commands.agents.delete import delete # noqa: E402
30
30
  from glaip_sdk.cli.commands.agents.get import get # noqa: E402
31
31
  from glaip_sdk.cli.commands.agents.list import list_agents # noqa: E402
32
- from glaip_sdk.cli.commands.agents.run import run, _maybe_attach_transcript_toggle # noqa: E402
32
+ from glaip_sdk.cli.commands.agents.run import _maybe_attach_transcript_toggle, run # noqa: E402
33
33
  from glaip_sdk.cli.commands.agents.sync_langflow import sync_langflow # noqa: E402
34
34
  from glaip_sdk.cli.commands.agents.update import update # noqa: E402
35
35
 
36
36
  # Import core functions for test compatibility
37
37
  from glaip_sdk.cli.core.context import get_client # noqa: E402
38
- from glaip_sdk.cli.core.rendering import with_client_and_spinner # noqa: E402
39
-
40
- # Import display functions for test compatibility
41
- from glaip_sdk.cli.display import ( # noqa: E402
42
- handle_json_output,
43
- handle_rich_output,
44
- )
45
38
 
46
39
  # Import core output functions for test compatibility
47
40
  from glaip_sdk.cli.core.output import ( # noqa: E402
@@ -52,11 +45,20 @@ from glaip_sdk.cli.core.output import ( # noqa: E402
52
45
  # Import rendering functions for test compatibility
53
46
  from glaip_sdk.cli.core.rendering import ( # noqa: E402
54
47
  build_renderer,
48
+ with_client_and_spinner, # noqa: E402
55
49
  )
56
50
 
57
51
  # Import display functions for test compatibility
58
- from glaip_sdk.cli.display import ( # noqa: E402
52
+ # Import display functions for test compatibility
53
+ from glaip_sdk.cli.display import ( # noqa: E402 # noqa: E402
59
54
  display_agent_run_suggestions,
55
+ handle_json_output,
56
+ handle_rich_output,
57
+ )
58
+
59
+ # Import IO functions for test compatibility
60
+ from glaip_sdk.cli.io import ( # noqa: E402
61
+ fetch_raw_resource_details,
60
62
  )
61
63
 
62
64
  # Import rich helpers for test compatibility
@@ -70,11 +72,6 @@ from glaip_sdk.cli.transcript import ( # noqa: E402
70
72
  store_transcript_for_session,
71
73
  )
72
74
 
73
- # Import IO functions for test compatibility
74
- from glaip_sdk.cli.io import ( # noqa: E402
75
- fetch_raw_resource_details,
76
- )
77
-
78
75
  # Import utils for test compatibility
79
76
  from glaip_sdk.utils import ( # noqa: E402
80
77
  is_uuid,
@@ -19,8 +19,11 @@ from glaip_sdk.branding import (
19
19
  WARNING_STYLE,
20
20
  )
21
21
  from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
22
- from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS, DEFAULT_MODEL
23
22
  from glaip_sdk.cli.context import get_ctx_value
23
+ from glaip_sdk.cli.core.output import (
24
+ output_result,
25
+ )
26
+ from glaip_sdk.cli.core.rendering import spinner_context
24
27
  from glaip_sdk.cli.display import (
25
28
  build_resource_result_data,
26
29
  handle_json_output,
@@ -31,10 +34,8 @@ from glaip_sdk.cli.hints import in_slash_mode
31
34
  from glaip_sdk.cli.io import fetch_raw_resource_details
32
35
  from glaip_sdk.cli.resolution import resolve_resource_reference
33
36
  from glaip_sdk.cli.rich_helpers import markup_text
34
- from glaip_sdk.cli.core.output import (
35
- output_result,
36
- )
37
- from glaip_sdk.cli.core.rendering import spinner_context
37
+ from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS
38
+ from glaip_sdk.models.constants import DEFAULT_MODEL
38
39
  from glaip_sdk.icons import ICON_AGENT
39
40
  from glaip_sdk.utils import format_datetime, is_uuid
40
41
 
@@ -11,19 +11,19 @@ from typing import Any
11
11
  import click
12
12
 
13
13
  from glaip_sdk.cli.context import output_flags
14
+ from glaip_sdk.cli.core.context import get_client
14
15
  from glaip_sdk.cli.display import (
15
16
  display_agent_run_suggestions,
16
17
  display_creation_success,
17
18
  handle_json_output,
18
19
  handle_rich_output,
19
20
  )
20
- from glaip_sdk.cli.core.context import get_client
21
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
22
21
  from glaip_sdk.cli.validators import (
23
22
  validate_agent_instruction_cli as validate_agent_instruction,
24
- validate_agent_name_cli as validate_agent_name,
25
- validate_timeout_cli as validate_timeout,
26
23
  )
24
+ from glaip_sdk.cli.validators import validate_agent_name_cli as validate_agent_name
25
+ from glaip_sdk.cli.validators import validate_timeout_cli as validate_timeout
26
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
27
27
  from glaip_sdk.utils.validation import coerce_timeout
28
28
 
29
29
  from ._common import (
@@ -67,7 +67,11 @@ def _handle_creation_exception(ctx: Any, e: Exception) -> None:
67
67
  @click.option("--instruction", help="Agent instruction (prompt)")
68
68
  @click.option(
69
69
  "--model",
70
- help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
70
+ help=(
71
+ "Language model in 'provider/model' format "
72
+ "(e.g., openai/gpt-4o-mini, bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0). "
73
+ "Use 'aip models list' to see available models."
74
+ ),
71
75
  )
72
76
  @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
73
77
  @click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
@@ -53,6 +53,10 @@ Screen {
53
53
  margin-left: 1;
54
54
  }
55
55
 
56
+ Button:hover {
57
+ background: $surface-lighten-1;
58
+ }
59
+
56
60
  #accounts-table {
57
61
  padding: 0 1 0 1;
58
62
  margin: 0 0 0 0;
@@ -60,6 +64,14 @@ Screen {
60
64
  border: tall $primary;
61
65
  }
62
66
 
67
+ #accounts-table > .datatable--row:hover {
68
+ background: $surface-lighten-1;
69
+ }
70
+
71
+ .sidebar-block:hover {
72
+ background: $surface-lighten-1;
73
+ }
74
+
63
75
  #status-bar {
64
76
  height: 3;
65
77
  padding: 0 1;
@@ -37,11 +37,19 @@ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
37
37
  from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
38
38
 
39
39
  try: # pragma: no cover - optional dependency
40
- from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastHandlerMixin, ToastVariant
40
+ from glaip_sdk.cli.slash.tui.toast import (
41
+ ClipboardToastMixin,
42
+ Toast,
43
+ ToastBus,
44
+ ToastContainer,
45
+ ToastHandlerMixin,
46
+ ToastVariant,
47
+ )
41
48
  except Exception: # pragma: no cover - optional dependency
42
49
  ClipboardToastMixin = object # type: ignore[assignment, misc]
43
50
  Toast = None # type: ignore[assignment]
44
51
  ToastBus = None # type: ignore[assignment]
52
+ ToastContainer = None # type: ignore[assignment]
45
53
  ToastHandlerMixin = object # type: ignore[assignment, misc]
46
54
  ToastVariant = None # type: ignore[assignment]
47
55
  from glaip_sdk.cli.validators import validate_api_key
@@ -51,7 +59,8 @@ try: # pragma: no cover - optional dependency
51
59
  from textual import events
52
60
  from textual.app import App, ComposeResult
53
61
  from textual.binding import Binding
54
- from textual.containers import Container, Horizontal, Vertical
62
+ from textual.containers import Horizontal, Vertical
63
+ from textual.coordinate import Coordinate
55
64
  from textual.screen import ModalScreen
56
65
  from textual.suggester import SuggestFromList
57
66
  from textual.widgets import Button, Checkbox, DataTable, Footer, Header, Input, LoadingIndicator, Static
@@ -60,9 +69,9 @@ except Exception: # pragma: no cover - optional dependency
60
69
  App = None # type: ignore[assignment]
61
70
  ComposeResult = None # type: ignore[assignment]
62
71
  Binding = None # type: ignore[assignment]
63
- Container = None # type: ignore[assignment]
64
72
  Horizontal = None # type: ignore[assignment]
65
73
  Vertical = None # type: ignore[assignment]
74
+ Coordinate = None # type: ignore[assignment]
66
75
  Button = None # type: ignore[assignment]
67
76
  Checkbox = None # type: ignore[assignment]
68
77
  DataTable = None # type: ignore[assignment]
@@ -527,7 +536,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
527
536
 
528
537
  def compose(self) -> ComposeResult: # type: ignore[return]
529
538
  """Compose the Harlequin layout with account list and detail panes."""
530
- if not TEXTUAL_SUPPORTED or Horizontal is None or Vertical is None or Container is None:
539
+ if not TEXTUAL_SUPPORTED or Horizontal is None or Vertical is None or Static is None:
531
540
  return # type: ignore[return-value]
532
541
 
533
542
  # Main container with horizontal split (25/75)
@@ -561,8 +570,8 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
561
570
  )
562
571
 
563
572
  # Toast container for notifications
564
- if Toast is not None:
565
- yield Container(Toast(), id="toast-container")
573
+ if Toast is not None and ToastContainer is not None:
574
+ yield ToastContainer(Toast(), id="toast-container")
566
575
 
567
576
  def on_mount(self) -> None:
568
577
  """Configure the screen after mount."""
@@ -833,6 +842,21 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
833
842
  if not self._is_switching:
834
843
  self.action_switch_account()
835
844
 
845
+ def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: # type: ignore[override]
846
+ """Handle mouse click selection by triggering switch."""
847
+ if not TEXTUAL_SUPPORTED:
848
+ return
849
+ table = self.query_one(HARLEQUIN_ACCOUNTS_LIST_ID, DataTable)
850
+ try:
851
+ table.cursor_coordinate = (event.coordinate.row, 0)
852
+ except Exception:
853
+ return
854
+ filtered = self._filtered_rows()
855
+ if event.coordinate.row < len(filtered):
856
+ self._update_selected_account(filtered[event.coordinate.row])
857
+ if not self._is_switching:
858
+ self.action_switch_account()
859
+
836
860
  def on_data_table_cursor_row_changed(self, event: DataTable.CursorRowChanged) -> None: # type: ignore[override]
837
861
  """Handle cursor movement in the accounts list."""
838
862
  if not TEXTUAL_SUPPORTED:
@@ -875,6 +899,8 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
875
899
  """Open edit account modal."""
876
900
  if self._check_env_lock():
877
901
  return
902
+ # Get account from cursor position if not explicitly selected
903
+ self._ensure_account_selected_from_cursor()
878
904
  name = self._get_selected_name()
879
905
  if not name:
880
906
  self._set_status("Select an account to edit.", "yellow")
@@ -897,6 +923,8 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
897
923
  """Open delete confirmation modal."""
898
924
  if self._check_env_lock():
899
925
  return
926
+ # Get account from cursor position if not explicitly selected
927
+ self._ensure_account_selected_from_cursor()
900
928
  name = self._get_selected_name()
901
929
  if not name:
902
930
  self._set_status("Select an account to delete.", "yellow")
@@ -987,7 +1015,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
987
1015
  if output is None:
988
1016
  return None
989
1017
 
990
- def _write(sequence: str, _output=output) -> None:
1018
+ def _write(sequence: str, _output: Any = output) -> None:
991
1019
  _output.write(sequence)
992
1020
  _output.flush()
993
1021
 
@@ -1310,20 +1338,33 @@ class AccountsTextualApp( # pragma: no cover - interactive
1310
1338
  self._queue_switch(name)
1311
1339
 
1312
1340
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: # type: ignore[override]
1313
- """Handle mouse click selection by triggering switch.
1341
+ """Handle row selection by triggering switch."""
1342
+ self._handle_table_click(self._event_row(event))
1314
1343
 
1315
- Note: This handler is for the old table layout. When using HarlequinScreen,
1316
- the screen handles row selection directly. This handler gracefully skips
1317
- if the old table doesn't exist.
1318
- """
1344
+ def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: # type: ignore[override]
1345
+ """Handle mouse click selection by triggering switch."""
1346
+ self._handle_table_click(self._event_row(event))
1347
+
1348
+ def _event_row(self, event: object) -> int | None:
1349
+ """Extract the row index from a DataTable event."""
1350
+ row = getattr(event, "cursor_row", None)
1351
+ if row is not None:
1352
+ return int(row)
1353
+ coordinate = getattr(event, "coordinate", None)
1354
+ return getattr(coordinate, "row", None) if coordinate is not None else None
1355
+
1356
+ def _handle_table_click(self, row: int | None) -> None:
1357
+ """Move the cursor to a clicked row and trigger the switch action."""
1358
+ if row is None:
1359
+ return
1319
1360
  try:
1320
1361
  table = self.query_one(ACCOUNTS_TABLE_ID, DataTable)
1321
1362
  except Exception:
1322
- # Harlequin screen is active, let it handle the event
1363
+ # Harlequin screen is active, let it handle the action
1323
1364
  return
1324
1365
  try:
1325
1366
  # Move cursor to clicked row then switch
1326
- table.cursor_coordinate = (event.cursor_row, 0)
1367
+ table.cursor_coordinate = Coordinate(row, 0)
1327
1368
  except Exception:
1328
1369
  return
1329
1370
  self.action_switch_row()
@@ -1654,7 +1695,7 @@ class AccountsTextualApp( # pragma: no cover - interactive
1654
1695
  if output is None:
1655
1696
  return None
1656
1697
 
1657
- def _write(sequence: str, _output=output) -> None:
1698
+ def _write(sequence: str, _output: Any = output) -> None:
1658
1699
  _output.write(sequence)
1659
1700
  _output.flush()
1660
1701
 
@@ -61,13 +61,18 @@ class TUIContext:
61
61
  store = get_account_store()
62
62
  settings = load_tui_settings(store=store)
63
63
 
64
- # Handle env var override: normalize empty strings and "default" to None
65
- # Empty string from os.getenv() is falsy, so strip() result becomes None in the or expression
66
64
  env_theme = os.getenv("AIP_TUI_THEME")
67
65
  env_theme = env_theme.strip() if env_theme else None
68
66
  if env_theme and env_theme.lower() == "default":
69
67
  env_theme = None
70
68
 
69
+ env_mouse = os.getenv("AIP_TUI_MOUSE_CAPTURE")
70
+ mouse_capture = settings.mouse_capture
71
+ if env_mouse is not None:
72
+ mouse_capture = env_mouse.lower() == "true"
73
+
74
+ terminal.mouse = mouse_capture
75
+
71
76
  theme_name = env_theme or settings.theme_name
72
77
  theme = ThemeManager(
73
78
  terminal,
@@ -19,8 +19,8 @@ from __future__ import annotations
19
19
  from typing import TYPE_CHECKING, Any
20
20
 
21
21
  try: # pragma: no cover - optional dependency
22
- from textual.containers import Container, Horizontal, Vertical
23
22
  from textual.screen import Screen
23
+ from textual.widget import Widget
24
24
  except Exception: # pragma: no cover - optional dependency
25
25
 
26
26
  class Screen: # type: ignore[no-redef]
@@ -30,23 +30,47 @@ except Exception: # pragma: no cover - optional dependency
30
30
  """Return the class for typing subscripts."""
31
31
  return cls
32
32
 
33
- Horizontal = None # type: ignore[assignment]
34
- Vertical = None # type: ignore[assignment]
35
- Container = None # type: ignore[assignment]
33
+ Widget = None # type: ignore[assignment]
36
34
 
37
35
  if TYPE_CHECKING:
38
36
  from glaip_sdk.cli.slash.tui.context import TUIContext
39
37
 
40
38
  try: # pragma: no cover - optional dependency
41
- from glaip_sdk.cli.slash.tui.toast import Toast
39
+ from glaip_sdk.cli.slash.tui.toast import Toast, ToastContainer
42
40
  except Exception: # pragma: no cover - optional dependency
43
41
  Toast = None # type: ignore[assignment, misc]
42
+ ToastContainer = None # type: ignore[assignment, misc]
44
43
 
45
44
  # GDP Labs Brand Palette
46
45
  PRIMARY_BLUE = "#005CB8"
47
46
  BLACK_BACKGROUND = "#000000"
48
47
 
49
48
 
49
+ if Widget is not None:
50
+
51
+ class HarlequinContainer(Widget):
52
+ """Base container for the Harlequin layout."""
53
+
54
+ DEFAULT_CSS = """
55
+ HarlequinContainer {
56
+ layout: horizontal;
57
+ }
58
+ """
59
+
60
+ class HarlequinPane(Widget):
61
+ """Pane container for Harlequin layout sections."""
62
+
63
+ DEFAULT_CSS = """
64
+ HarlequinPane {
65
+ layout: vertical;
66
+ }
67
+ """
68
+
69
+ else:
70
+ HarlequinContainer = None # type: ignore[assignment, misc]
71
+ HarlequinPane = None # type: ignore[assignment, misc]
72
+
73
+
50
74
  class HarlequinScreen(Screen[None]): # type: ignore[misc]
51
75
  """Base class for Harlequin-style multi-pane screens.
52
76
 
@@ -136,19 +160,19 @@ class HarlequinScreen(Screen[None]): # type: ignore[misc]
136
160
  Returns:
137
161
  ComposeResult yielding the base layout containers.
138
162
  """
139
- if Horizontal is None or Vertical is None or Container is None:
163
+ if HarlequinContainer is None or HarlequinPane is None:
140
164
  return
141
165
 
142
166
  # Main container with horizontal split (25/75)
143
- yield Horizontal(
144
- Vertical(id="left-pane"),
145
- Vertical(id="right-pane"),
167
+ yield HarlequinContainer(
168
+ HarlequinPane(id="left-pane"),
169
+ HarlequinPane(id="right-pane"),
146
170
  id="harlequin-container",
147
171
  )
148
172
 
149
173
  # Toast container for notifications
150
- if Toast is not None and Container is not None:
151
- yield Container(Toast(), id="toast-container")
174
+ if Toast is not None and ToastContainer is not None:
175
+ yield ToastContainer(Toast(), id="toast-container")
152
176
 
153
177
  @property
154
178
  def ctx(self) -> TUIContext | None: