glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,235 @@
1
+ """Keybinding registry helpers for TUI applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+
8
+ DEFAULT_LEADER = "ctrl+x"
9
+ _LEADER_TOKEN = "<leader>"
10
+
11
+ _MODIFIER_ORDER = ("ctrl", "alt", "shift", "meta")
12
+ _MODIFIER_SYNONYMS = {
13
+ "control": "ctrl",
14
+ "ctl": "ctrl",
15
+ "cmd": "meta",
16
+ "command": "meta",
17
+ "option": "alt",
18
+ "return": "enter",
19
+ }
20
+
21
+ _KEY_SYNONYMS = {
22
+ "esc": "escape",
23
+ }
24
+
25
+ _KEY_DISPLAY = {
26
+ "escape": "Esc",
27
+ "enter": "Enter",
28
+ "space": "Space",
29
+ "tab": "Tab",
30
+ "backspace": "Backspace",
31
+ }
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Keybind:
36
+ """A registered keybinding."""
37
+
38
+ action: str
39
+ sequence: tuple[str, ...]
40
+ description: str
41
+ category: str | None = None
42
+
43
+ def __repr__(self) -> str:
44
+ """Return a readable representation of the keybind."""
45
+ return (
46
+ f"Keybind(action={self.action!r}, sequence={self.sequence}, "
47
+ f"description={self.description!r}, category={self.category!r})"
48
+ )
49
+
50
+
51
+ class KeybindRegistry:
52
+ """Central registry of keybindings and associated metadata."""
53
+
54
+ def __init__(self, *, leader: str = DEFAULT_LEADER) -> None:
55
+ """Initialize the registry."""
56
+ normalized = _normalize_chord(leader)
57
+ self._leader = normalized or DEFAULT_LEADER
58
+ self._keybinds: dict[str, Keybind] = {}
59
+
60
+ @property
61
+ def leader(self) -> str:
62
+ """Return the normalized leader chord."""
63
+ return self._leader
64
+
65
+ def register(
66
+ self,
67
+ *,
68
+ action: str,
69
+ key: str,
70
+ description: str,
71
+ category: str | None = None,
72
+ ) -> Keybind:
73
+ """Register a keybinding for an action."""
74
+ if action in self._keybinds:
75
+ raise ValueError(f"Action already registered: {action}")
76
+
77
+ sequence = parse_key_sequence(key)
78
+ keybind = Keybind(action=action, sequence=sequence, description=description, category=category)
79
+ self._keybinds[action] = keybind
80
+ return keybind
81
+
82
+ def get(self, action: str) -> Keybind | None:
83
+ """Return keybind for action, if present."""
84
+ return self._keybinds.get(action)
85
+
86
+ def actions(self) -> list[str]:
87
+ """Return sorted list of registered actions."""
88
+ return sorted(self._keybinds)
89
+
90
+ def matches(self, action: str, sequence: str | Iterable[str]) -> bool:
91
+ """Return True if the provided sequence matches the action's keybind."""
92
+ keybind = self._keybinds.get(action)
93
+ if keybind is None:
94
+ return False
95
+
96
+ candidate = _coerce_sequence(sequence)
97
+ return candidate == keybind.sequence
98
+
99
+ def format(self, action: str) -> str:
100
+ """Return a human-readable sequence for an action."""
101
+ keybind = self._keybinds.get(action)
102
+ if keybind is None:
103
+ return ""
104
+
105
+ return format_key_sequence(keybind.sequence, leader=self._leader)
106
+
107
+
108
+ def parse_key_sequence(key: str) -> tuple[str, ...]:
109
+ """Parse a key sequence string into normalized tokens."""
110
+ tokens = [token for token in key.strip().split() if token]
111
+ normalized: list[str] = []
112
+
113
+ for token in tokens:
114
+ if token.lower() == _LEADER_TOKEN:
115
+ normalized.append(_LEADER_TOKEN)
116
+ continue
117
+
118
+ chord = _normalize_chord(token)
119
+ if chord:
120
+ normalized.append(chord)
121
+
122
+ return tuple(normalized)
123
+
124
+
125
+ def format_key_sequence(sequence: tuple[str, ...], *, leader: str = DEFAULT_LEADER) -> str:
126
+ """Format a normalized sequence into a display string."""
127
+ rendered: list[str] = []
128
+
129
+ for token in sequence:
130
+ if token == _LEADER_TOKEN:
131
+ rendered.append(_format_token(leader))
132
+ continue
133
+ rendered.append(_format_token(token))
134
+
135
+ return " ".join(rendered)
136
+
137
+
138
+ def _coerce_sequence(sequence: str | Iterable[str]) -> tuple[str, ...]:
139
+ if isinstance(sequence, str):
140
+ return parse_key_sequence(sequence)
141
+
142
+ tokens: list[str] = []
143
+ for token in sequence:
144
+ if not token:
145
+ continue
146
+ if token.lower() == _LEADER_TOKEN:
147
+ tokens.append(_LEADER_TOKEN)
148
+ continue
149
+ chord = _normalize_chord(token)
150
+ if chord:
151
+ tokens.append(chord)
152
+
153
+ return tuple(tokens)
154
+
155
+
156
+ def _normalize_chord(chord: str) -> str:
157
+ """Normalize a key chord string to canonical form.
158
+
159
+ Normalization rules:
160
+ - Converts separators: both '-' and '+' are normalized to '+'
161
+ - Handles synonyms: 'control'/'ctl' -> 'ctrl', 'cmd'/'command' -> 'meta', 'option' -> 'alt'
162
+ - Deduplicates modifiers: 'ctrl+ctrl+l' -> 'ctrl+l'
163
+ - Orders modifiers: ctrl < alt < shift < meta (unknown modifiers sort last)
164
+ - Case-insensitive: 'Ctrl+L' == 'ctrl+l' == 'CTRL-L'
165
+
166
+ Args:
167
+ chord: Key chord string (e.g., "Ctrl+L", "ctrl-l", "CTRL+CTRL+L")
168
+
169
+ Returns:
170
+ Normalized chord string (e.g., "ctrl+l") or empty string if invalid.
171
+ """
172
+ parts = [part for part in chord.replace("-", "+").split("+") if part.strip()]
173
+ if not parts:
174
+ return ""
175
+
176
+ normalized_parts = [_normalize_key_part(part) for part in parts]
177
+ if len(normalized_parts) == 1:
178
+ return normalized_parts[0]
179
+
180
+ modifiers, key = normalized_parts[:-1], normalized_parts[-1]
181
+
182
+ seen: set[str] = set()
183
+ unique_mods: list[str] = []
184
+ for mod in modifiers:
185
+ if mod in seen:
186
+ continue
187
+ seen.add(mod)
188
+ unique_mods.append(mod)
189
+
190
+ unique_mods.sort(key=_modifier_sort_key)
191
+ return "+".join([*unique_mods, key])
192
+
193
+
194
+ def _normalize_key_part(part: str) -> str:
195
+ token = part.strip().lower()
196
+ token = _MODIFIER_SYNONYMS.get(token, token)
197
+ return _KEY_SYNONYMS.get(token, token)
198
+
199
+
200
+ def _modifier_sort_key(modifier: str) -> int:
201
+ try:
202
+ return _MODIFIER_ORDER.index(modifier)
203
+ except ValueError:
204
+ return len(_MODIFIER_ORDER)
205
+
206
+
207
+ def _format_token(token: str) -> str:
208
+ if "+" in token:
209
+ return _format_chord(token)
210
+
211
+ return _KEY_DISPLAY.get(token, token)
212
+
213
+
214
+ def _format_chord(chord: str) -> str:
215
+ parts = chord.split("+")
216
+ modifiers, key = parts[:-1], parts[-1]
217
+
218
+ rendered_mods: list[str] = []
219
+ for mod in modifiers:
220
+ if mod == "ctrl":
221
+ rendered_mods.append("Ctrl")
222
+ elif mod == "alt":
223
+ rendered_mods.append("Alt")
224
+ elif mod == "shift":
225
+ rendered_mods.append("Shift")
226
+ elif mod == "meta":
227
+ rendered_mods.append("Meta")
228
+ else:
229
+ rendered_mods.append(mod.title())
230
+
231
+ rendered_key = _KEY_DISPLAY.get(key, key)
232
+ if len(rendered_key) == 1 and rendered_key.isalpha():
233
+ rendered_key = rendered_key.upper()
234
+
235
+ return "+".join([*rendered_mods, rendered_key])
@@ -0,0 +1,14 @@
1
+ """Layout components for TUI applications.
2
+
3
+ This package provides reusable layout components following the Harlequin pattern
4
+ for multi-pane data-rich screens.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ try: # pragma: no cover - optional dependency
10
+ from glaip_sdk.cli.slash.tui.layouts.harlequin import HarlequinScreen
11
+ except Exception: # pragma: no cover - optional dependency
12
+ HarlequinScreen = None # type: ignore[assignment, misc]
13
+
14
+ __all__ = ["HarlequinScreen"]
@@ -0,0 +1,184 @@
1
+ """Harlequin layout base class for multi-pane TUI screens.
2
+
3
+ This module provides the HarlequinScreen base class, which implements a modern
4
+ multi-pane "Harlequin" layout pattern for data-rich TUI screens. The layout uses
5
+ a 25/75 split with a list on the left and detail content on the right.
6
+
7
+ The Harlequin pattern is inspired by the Harlequin SQL client and provides:
8
+ - Left Pane (25%): ListView or compact table for item selection
9
+ - Right Pane (75%): Detail dashboard showing all fields, status, and action buttons
10
+ - Black background (#000000) that overrides terminal transparency
11
+ - Primary Blue borders (#005CB8)
12
+
13
+ Authors:
14
+ Raymond Christopher (raymond.christopher@gdplabs.id)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ try: # pragma: no cover - optional dependency
22
+ from textual.screen import Screen
23
+ from textual.widget import Widget
24
+ except Exception: # pragma: no cover - optional dependency
25
+
26
+ class Screen: # type: ignore[no-redef]
27
+ """Fallback Screen stub when Textual is unavailable."""
28
+
29
+ def __class_getitem__(cls, _):
30
+ """Return the class for typing subscripts."""
31
+ return cls
32
+
33
+ Widget = None # type: ignore[assignment]
34
+
35
+ if TYPE_CHECKING:
36
+ from glaip_sdk.cli.slash.tui.context import TUIContext
37
+
38
+ try: # pragma: no cover - optional dependency
39
+ from glaip_sdk.cli.slash.tui.toast import Toast, ToastContainer
40
+ except Exception: # pragma: no cover - optional dependency
41
+ Toast = None # type: ignore[assignment, misc]
42
+ ToastContainer = None # type: ignore[assignment, misc]
43
+
44
+ # GDP Labs Brand Palette
45
+ PRIMARY_BLUE = "#005CB8"
46
+ BLACK_BACKGROUND = "#000000"
47
+
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
+
74
+ class HarlequinScreen(Screen[None]): # type: ignore[misc]
75
+ """Base class for Harlequin-style multi-pane screens.
76
+
77
+ This screen provides a 25/75 split layout with a left pane for navigation
78
+ and a right pane for details. The layout uses a black background that
79
+ overrides terminal transparency and primary blue borders.
80
+
81
+ Subclasses should override `compose()` to add their specific widgets to
82
+ the left and right panes. Use the container IDs "left-pane" and "right-pane"
83
+ to target specific panes in CSS or when querying widgets.
84
+
85
+ Example:
86
+ ```python
87
+ class AccountHarlequinScreen(HarlequinScreen):
88
+ def compose(self) -> ComposeResult:
89
+ yield from super().compose()
90
+ # Add widgets to left and right panes
91
+ self.query_one("#left-pane").mount(AccountListView())
92
+ self.query_one("#right-pane").mount(AccountDetailView())
93
+ ```
94
+
95
+ CSS:
96
+ The screen includes default styling for the Harlequin layout:
97
+ - Black background (#000000) for the entire screen
98
+ - Primary blue borders (#005CB8) for panes
99
+ - 25% width for left pane, 75% width for right pane
100
+ """
101
+
102
+ CSS = """
103
+ HarlequinScreen {
104
+ background: #000000;
105
+ layers: base toasts;
106
+ }
107
+
108
+ #harlequin-container {
109
+ width: 100%;
110
+ height: 100%;
111
+ }
112
+
113
+ #left-pane {
114
+ width: 25%;
115
+ border: solid #005CB8;
116
+ background: #000000;
117
+ }
118
+
119
+ #right-pane {
120
+ width: 75%;
121
+ border: solid #005CB8;
122
+ background: #000000;
123
+ }
124
+
125
+ #toast-container {
126
+ width: 100%;
127
+ height: auto;
128
+ dock: top;
129
+ align: right top;
130
+ layer: toasts;
131
+ }
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ *,
137
+ ctx: TUIContext | None = None,
138
+ name: str | None = None,
139
+ id: str | None = None,
140
+ classes: str | None = None,
141
+ ) -> None:
142
+ """Initialize the Harlequin screen.
143
+
144
+ Args:
145
+ ctx: Optional TUI context for accessing services (keybinds, theme, toasts, clipboard).
146
+ name: Optional name for the screen.
147
+ id: Optional ID for the screen.
148
+ classes: Optional CSS classes for the screen.
149
+ """
150
+ super().__init__(name=name, id=id, classes=classes)
151
+ self._ctx: TUIContext | None = ctx
152
+
153
+ def compose(self) -> Any:
154
+ """Compose the Harlequin layout with left and right panes.
155
+
156
+ This method creates the base 25/75 split layout. Subclasses should
157
+ call `super().compose()` and then add their specific widgets to the
158
+ left and right panes.
159
+
160
+ Returns:
161
+ ComposeResult yielding the base layout containers.
162
+ """
163
+ if HarlequinContainer is None or HarlequinPane is None:
164
+ return
165
+
166
+ # Main container with horizontal split (25/75)
167
+ yield HarlequinContainer(
168
+ HarlequinPane(id="left-pane"),
169
+ HarlequinPane(id="right-pane"),
170
+ id="harlequin-container",
171
+ )
172
+
173
+ # Toast container for notifications
174
+ if Toast is not None and ToastContainer is not None:
175
+ yield ToastContainer(Toast(), id="toast-container")
176
+
177
+ @property
178
+ def ctx(self) -> TUIContext | None:
179
+ """Get the TUI context if available.
180
+
181
+ Returns:
182
+ TUIContext instance or None if not provided.
183
+ """
184
+ return self._ctx
@@ -0,0 +1,80 @@
1
+ """Shared helpers for toggling Textual loading indicators.
2
+
3
+ This module provides unified helpers for showing/hiding both the built-in
4
+ Textual LoadingIndicator and the custom PulseIndicator.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from textual.widgets import LoadingIndicator
13
+
14
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
15
+
16
+
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
+ return
27
+ except Exception:
28
+ pass
29
+
30
+ try:
31
+ indicator = app.query_one(selector, LoadingIndicator)
32
+ indicator.display = visible
33
+ except Exception:
34
+ return
35
+
36
+
37
+ def show_loading_indicator(
38
+ app: Any,
39
+ selector: str,
40
+ *,
41
+ message: str | None = None,
42
+ set_status: Callable[..., None] | None = None,
43
+ status_style: str = "cyan",
44
+ ) -> None:
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
+ """
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
+
63
+ if message and set_status:
64
+ try:
65
+ set_status(message, status_style)
66
+ except TypeError:
67
+ try:
68
+ set_status(message)
69
+ except Exception:
70
+ return
71
+
72
+
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
+ """
80
+ _set_indicator_display(app, selector, False)