glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. glaip_sdk/agents/base.py +283 -30
  2. glaip_sdk/agents/component.py +233 -0
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +1 -1
  17. glaip_sdk/cli/commands/configure.py +1 -2
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/entrypoint.py +20 -0
  46. glaip_sdk/cli/main.py +112 -35
  47. glaip_sdk/cli/pager.py +3 -3
  48. glaip_sdk/cli/resolution.py +2 -1
  49. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  50. glaip_sdk/cli/slash/agent_session.py +1 -1
  51. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  52. glaip_sdk/cli/slash/session.py +343 -20
  53. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  54. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  55. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  56. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  57. glaip_sdk/cli/slash/tui/context.py +92 -0
  58. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  59. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  60. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  61. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  62. glaip_sdk/cli/slash/tui/loading.py +43 -21
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +388 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +1 -1
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +293 -17
  78. glaip_sdk/client/base.py +25 -0
  79. glaip_sdk/client/hitl.py +136 -0
  80. glaip_sdk/client/main.py +7 -5
  81. glaip_sdk/client/mcps.py +44 -13
  82. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  83. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  84. glaip_sdk/client/payloads/agent/responses.py +43 -0
  85. glaip_sdk/client/run_rendering.py +109 -30
  86. glaip_sdk/client/schedules.py +439 -0
  87. glaip_sdk/client/tools.py +52 -23
  88. glaip_sdk/config/constants.py +22 -2
  89. glaip_sdk/guardrails/__init__.py +80 -0
  90. glaip_sdk/guardrails/serializer.py +91 -0
  91. glaip_sdk/hitl/__init__.py +35 -2
  92. glaip_sdk/hitl/base.py +64 -0
  93. glaip_sdk/hitl/callback.py +43 -0
  94. glaip_sdk/hitl/local.py +1 -31
  95. glaip_sdk/hitl/remote.py +523 -0
  96. glaip_sdk/models/__init__.py +47 -1
  97. glaip_sdk/models/_provider_mappings.py +101 -0
  98. glaip_sdk/models/_validation.py +97 -0
  99. glaip_sdk/models/agent.py +2 -1
  100. glaip_sdk/models/agent_runs.py +2 -1
  101. glaip_sdk/models/constants.py +141 -0
  102. glaip_sdk/models/model.py +170 -0
  103. glaip_sdk/models/schedule.py +224 -0
  104. glaip_sdk/payload_schemas/agent.py +1 -0
  105. glaip_sdk/payload_schemas/guardrails.py +34 -0
  106. glaip_sdk/ptc.py +145 -0
  107. glaip_sdk/registry/tool.py +270 -57
  108. glaip_sdk/runner/__init__.py +20 -3
  109. glaip_sdk/runner/deps.py +4 -1
  110. glaip_sdk/runner/langgraph.py +251 -27
  111. glaip_sdk/runner/logging_config.py +77 -0
  112. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  113. glaip_sdk/runner/ptc_adapter.py +98 -0
  114. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  115. glaip_sdk/schedules/__init__.py +22 -0
  116. glaip_sdk/schedules/base.py +291 -0
  117. glaip_sdk/tools/base.py +67 -14
  118. glaip_sdk/utils/__init__.py +1 -0
  119. glaip_sdk/utils/agent_config.py +8 -2
  120. glaip_sdk/utils/bundler.py +138 -2
  121. glaip_sdk/utils/import_resolver.py +427 -49
  122. glaip_sdk/utils/runtime_config.py +3 -2
  123. glaip_sdk/utils/sync.py +31 -11
  124. glaip_sdk/utils/tool_detection.py +274 -6
  125. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
  126. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  127. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
  128. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  129. glaip_sdk/cli/commands/agents.py +0 -1509
  130. glaip_sdk/cli/commands/mcps.py +0 -1356
  131. glaip_sdk/cli/commands/tools.py +0 -576
  132. glaip_sdk/cli/utils.py +0 -263
  133. glaip_sdk-0.6.19.dist-info/RECORD +0 -163
  134. glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
  135. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,316 @@
1
+ """Clipboard adapter for TUI copy actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import Any
14
+ from collections.abc import Callable
15
+
16
+ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_osc52_support
17
+
18
+
19
+ class ClipboardMethod(str, Enum):
20
+ """Supported clipboard backends."""
21
+
22
+ OSC52 = "osc52"
23
+ PBCOPY = "pbcopy"
24
+ XCLIP = "xclip"
25
+ XSEL = "xsel"
26
+ WL_COPY = "wl-copy"
27
+ CLIP = "clip"
28
+ NONE = "none"
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ClipboardResult:
33
+ """Result of a clipboard operation."""
34
+
35
+ success: bool
36
+ method: ClipboardMethod
37
+ message: str
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class ClipboardReadResult:
42
+ """Result of a clipboard read operation."""
43
+
44
+ success: bool
45
+ method: ClipboardMethod
46
+ message: str
47
+ text: str
48
+
49
+
50
+ _SUBPROCESS_COMMANDS: dict[ClipboardMethod, list[str]] = {
51
+ ClipboardMethod.PBCOPY: ["pbcopy"],
52
+ ClipboardMethod.XCLIP: ["xclip", "-selection", "clipboard"],
53
+ ClipboardMethod.XSEL: ["xsel", "--clipboard", "--input"],
54
+ ClipboardMethod.WL_COPY: ["wl-copy"],
55
+ ClipboardMethod.CLIP: ["clip"],
56
+ }
57
+ _SUBPROCESS_READ_COMMANDS: dict[ClipboardMethod, list[str]] = {
58
+ ClipboardMethod.PBCOPY: ["pbpaste"],
59
+ ClipboardMethod.XCLIP: ["xclip", "-selection", "clipboard", "-o"],
60
+ ClipboardMethod.XSEL: ["xsel", "--clipboard", "--output"],
61
+ ClipboardMethod.WL_COPY: ["wl-paste", "--no-newline"],
62
+ }
63
+
64
+ _ENV_CLIPBOARD_METHOD = "AIP_TUI_CLIPBOARD_METHOD"
65
+ _ENV_CLIPBOARD_FORCE = "AIP_TUI_CLIPBOARD_FORCE"
66
+ _ENV_METHOD_MAP = {
67
+ "osc52": ClipboardMethod.OSC52,
68
+ "pbcopy": ClipboardMethod.PBCOPY,
69
+ "xclip": ClipboardMethod.XCLIP,
70
+ "xsel": ClipboardMethod.XSEL,
71
+ "wl-copy": ClipboardMethod.WL_COPY,
72
+ "wl_copy": ClipboardMethod.WL_COPY,
73
+ "clip": ClipboardMethod.CLIP,
74
+ "none": ClipboardMethod.NONE,
75
+ }
76
+
77
+ _SUBPROCESS_TIMEOUT = 2.0
78
+
79
+
80
+ def _resolve_env_method() -> ClipboardMethod | None:
81
+ raw = os.getenv(_ENV_CLIPBOARD_METHOD)
82
+ if not raw:
83
+ return None
84
+ value = raw.strip().lower()
85
+ if value in ("auto", "default"):
86
+ return None
87
+ return _ENV_METHOD_MAP.get(value)
88
+
89
+
90
+ def _is_env_force_enabled() -> bool:
91
+ raw = os.getenv(_ENV_CLIPBOARD_FORCE)
92
+ if not raw:
93
+ return False
94
+ return raw.strip().lower() in {"1", "true", "yes", "on"}
95
+
96
+
97
+ def _resolve_windows_read_command() -> list[str] | None:
98
+ for shell in ("powershell", "pwsh"):
99
+ if shutil.which(shell):
100
+ return [shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"]
101
+ return None
102
+
103
+
104
+ class ClipboardAdapter:
105
+ """Cross-platform clipboard access with OSC 52 fallback."""
106
+
107
+ def __init__(
108
+ self,
109
+ *,
110
+ terminal: TerminalCapabilities | None = None,
111
+ method: ClipboardMethod | None = None,
112
+ ) -> None:
113
+ """Initialize the adapter."""
114
+ self._terminal = terminal
115
+ self._force_method = False
116
+ self._fallback_methods_cache: list[ClipboardMethod] | None = None
117
+ if method is not None:
118
+ self._method = method
119
+ else:
120
+ env_method = _resolve_env_method()
121
+ if env_method is not None:
122
+ self._method = env_method
123
+ self._force_method = _is_env_force_enabled()
124
+ else:
125
+ self._method = self._detect_method()
126
+
127
+ @property
128
+ def method(self) -> ClipboardMethod:
129
+ """Return the detected clipboard backend."""
130
+ return self._method
131
+
132
+ def copy(self, text: str, *, writer: Callable[[str], Any] | None = None) -> ClipboardResult:
133
+ """Copy text to clipboard using the best available method.
134
+
135
+ Args:
136
+ text: Text to copy.
137
+ writer: Optional function to write OSC 52 sequence (e.g., self.app.console.write).
138
+ Defaults to sys.stdout.write if not provided.
139
+ """
140
+ if self._method == ClipboardMethod.OSC52:
141
+ return self._copy_osc52(text, writer=writer)
142
+
143
+ command = _SUBPROCESS_COMMANDS.get(self._method)
144
+ if command is None:
145
+ if self._force_method:
146
+ return ClipboardResult(False, self._method, "Forced clipboard method unavailable.")
147
+ return self._copy_osc52(text, writer=writer)
148
+
149
+ result = self._copy_subprocess(command, text)
150
+ if not result.success:
151
+ if self._force_method or "timed out" in result.message:
152
+ return result
153
+ return self._copy_osc52(text, writer=writer)
154
+
155
+ return result
156
+
157
+ def read(self) -> ClipboardReadResult:
158
+ """Read text from the clipboard using the best available method."""
159
+ result = self._read_with_method(self._method)
160
+ if result.success or self._force_method:
161
+ return result
162
+
163
+ if self._fallback_methods_cache is None:
164
+ self._fallback_methods_cache = self._fallback_read_methods()
165
+
166
+ for method in self._fallback_methods_cache:
167
+ if method is self._method:
168
+ continue
169
+ fallback = self._read_with_method(method)
170
+ if fallback.success:
171
+ return fallback
172
+
173
+ return result
174
+
175
+ def _detect_method(self) -> ClipboardMethod:
176
+ system = platform.system()
177
+ method = ClipboardMethod.NONE
178
+ if system == "Darwin":
179
+ method = self._detect_darwin_method()
180
+ elif system == "Linux":
181
+ method = self._detect_linux_method()
182
+ elif system == "Windows":
183
+ method = self._detect_windows_method()
184
+
185
+ if method is not ClipboardMethod.NONE:
186
+ return method
187
+
188
+ if self._terminal.osc52 if self._terminal else detect_osc52_support():
189
+ return ClipboardMethod.OSC52
190
+
191
+ return ClipboardMethod.NONE
192
+
193
+ def _detect_darwin_method(self) -> ClipboardMethod:
194
+ return ClipboardMethod.PBCOPY if shutil.which("pbcopy") else ClipboardMethod.NONE
195
+
196
+ def _detect_linux_method(self) -> ClipboardMethod:
197
+ if not os.getenv("DISPLAY") and not os.getenv("WAYLAND_DISPLAY"):
198
+ return ClipboardMethod.NONE
199
+
200
+ # Order of preference: Wayland then X11 tools
201
+ for method in (ClipboardMethod.WL_COPY, ClipboardMethod.XCLIP, ClipboardMethod.XSEL):
202
+ cmd = _SUBPROCESS_COMMANDS.get(method)
203
+ if cmd and shutil.which(cmd[0]):
204
+ return method
205
+ return ClipboardMethod.NONE
206
+
207
+ def _detect_windows_method(self) -> ClipboardMethod:
208
+ return ClipboardMethod.CLIP if shutil.which("clip") else ClipboardMethod.NONE
209
+
210
+ def _copy_osc52(self, text: str, *, writer: Callable[[str], Any] | None = None) -> ClipboardResult:
211
+ encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
212
+ sequence = f"\x1b]52;c;{encoded}\x07"
213
+ try:
214
+ if writer:
215
+ writer(sequence)
216
+ else:
217
+ sys.stdout.write(sequence)
218
+ sys.stdout.flush()
219
+ except Exception as exc:
220
+ return ClipboardResult(False, ClipboardMethod.OSC52, str(exc))
221
+
222
+ return ClipboardResult(True, ClipboardMethod.OSC52, "Copied to clipboard")
223
+
224
+ def _copy_subprocess(self, cmd: list[str], text: str) -> ClipboardResult:
225
+ try:
226
+ completed = subprocess.run(
227
+ cmd,
228
+ input=text.encode("utf-8"),
229
+ check=False,
230
+ timeout=_SUBPROCESS_TIMEOUT,
231
+ )
232
+ except subprocess.TimeoutExpired:
233
+ return ClipboardResult(False, self._method, f"Clipboard command timed out after {_SUBPROCESS_TIMEOUT}s")
234
+ except OSError as exc:
235
+ return ClipboardResult(False, self._method, str(exc))
236
+
237
+ if completed.returncode == 0:
238
+ return ClipboardResult(True, self._method, "Copied to clipboard")
239
+
240
+ return ClipboardResult(False, self._method, f"Command failed: {completed.returncode}")
241
+
242
+ def _read_with_method(self, method: ClipboardMethod) -> ClipboardReadResult:
243
+ if method is ClipboardMethod.OSC52:
244
+ # OSC 52 read requires an asynchronous terminal response (DSR) which is
245
+ # significantly more complex to implement than synchronous subprocess reads.
246
+ # Currently out of scope.
247
+ return ClipboardReadResult(False, method, "OSC 52 clipboard read is unsupported.", "")
248
+ if method is ClipboardMethod.NONE:
249
+ return ClipboardReadResult(False, method, "Clipboard backend unavailable.", "")
250
+
251
+ if method is ClipboardMethod.CLIP:
252
+ command = _resolve_windows_read_command()
253
+ if command is None:
254
+ return ClipboardReadResult(False, method, "PowerShell clipboard read unavailable.", "")
255
+ return self._read_subprocess(command, method)
256
+
257
+ command = _SUBPROCESS_READ_COMMANDS.get(method)
258
+ if command is None:
259
+ return ClipboardReadResult(False, method, "Clipboard read method unavailable.", "")
260
+
261
+ return self._read_subprocess(command, method)
262
+
263
+ def _read_subprocess(self, cmd: list[str], method: ClipboardMethod) -> ClipboardReadResult:
264
+ try:
265
+ completed = subprocess.run(
266
+ cmd,
267
+ capture_output=True,
268
+ text=True,
269
+ encoding="utf-8",
270
+ check=False,
271
+ timeout=_SUBPROCESS_TIMEOUT,
272
+ )
273
+ except subprocess.TimeoutExpired:
274
+ return ClipboardReadResult(False, method, f"Clipboard command timed out after {_SUBPROCESS_TIMEOUT}s", "")
275
+ except OSError as exc:
276
+ return ClipboardReadResult(False, method, str(exc), "")
277
+
278
+ if completed.returncode == 0:
279
+ return ClipboardReadResult(True, method, "Read from clipboard", completed.stdout)
280
+
281
+ return ClipboardReadResult(False, method, f"Command failed: {completed.returncode}", "")
282
+
283
+ def _fallback_read_methods(self) -> list[ClipboardMethod]:
284
+ system = platform.system()
285
+ if system == "Darwin":
286
+ return self._fallback_darwin()
287
+ if system == "Linux":
288
+ return self._fallback_linux()
289
+ if system == "Windows":
290
+ return self._fallback_windows()
291
+ return []
292
+
293
+ def _fallback_darwin(self) -> list[ClipboardMethod]:
294
+ cmd = _SUBPROCESS_READ_COMMANDS.get(ClipboardMethod.PBCOPY)
295
+ if cmd and shutil.which(cmd[0]):
296
+ return [ClipboardMethod.PBCOPY]
297
+ return []
298
+
299
+ def _fallback_linux(self) -> list[ClipboardMethod]:
300
+ methods: list[ClipboardMethod] = []
301
+ for method in (ClipboardMethod.WL_COPY, ClipboardMethod.XCLIP, ClipboardMethod.XSEL):
302
+ cmd = _SUBPROCESS_READ_COMMANDS.get(method)
303
+ if not cmd:
304
+ continue
305
+ if method == ClipboardMethod.WL_COPY and not os.getenv("WAYLAND_DISPLAY"):
306
+ continue
307
+ if method in (ClipboardMethod.XCLIP, ClipboardMethod.XSEL) and not os.getenv("DISPLAY"):
308
+ continue
309
+ if shutil.which(cmd[0]):
310
+ methods.append(method)
311
+ return methods
312
+
313
+ def _fallback_windows(self) -> list[ClipboardMethod]:
314
+ if _resolve_windows_read_command():
315
+ return [ClipboardMethod.CLIP]
316
+ return []
@@ -0,0 +1,92 @@
1
+ """Shared context for all TUI components.
2
+
3
+ This module provides the TUIContext dataclass, which serves as the Python equivalent
4
+ of OpenCode's nested provider pattern. It provides a single container for all TUI
5
+ services and state that can be injected into components.
6
+
7
+ Authors:
8
+ Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from dataclasses import dataclass
15
+
16
+ from glaip_sdk.cli.account_store import get_account_store
17
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
18
+ from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
19
+ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
20
+ from glaip_sdk.cli.slash.tui.theme import ThemeManager
21
+ from glaip_sdk.cli.slash.tui.toast import ToastBus
22
+ from glaip_sdk.cli.tui_settings import load_tui_settings
23
+
24
+
25
+ @dataclass
26
+ class TUIContext:
27
+ """Shared context for all TUI components (Python equivalent of OpenCode's providers).
28
+
29
+ This context provides access to all TUI services and state. Components that will
30
+ be implemented in later phases are typed as Optional and will be None initially.
31
+
32
+ Attributes:
33
+ terminal: Terminal capability detection results.
34
+ keybinds: Central keybind registry (Phase 3).
35
+ theme: Theme manager for light/dark mode and color tokens (Phase 2).
36
+ toasts: Toast notification bus (Phase 4).
37
+ clipboard: Clipboard adapter with OSC 52 support (Phase 4).
38
+ """
39
+
40
+ terminal: TerminalCapabilities
41
+ keybinds: KeybindRegistry | None = None
42
+ theme: ThemeManager | None = None
43
+ toasts: ToastBus | None = None
44
+ clipboard: ClipboardAdapter | None = None
45
+
46
+ @classmethod
47
+ async def create(cls, *, detect_osc11: bool = True) -> TUIContext:
48
+ """Create a TUIContext instance with detected terminal capabilities.
49
+
50
+ This factory method detects terminal capabilities asynchronously and
51
+ returns a populated TUIContext instance with all services initialized
52
+ (keybinds, theme, toasts, clipboard).
53
+
54
+ Args:
55
+ detect_osc11: When False, skip OSC 11 background detection.
56
+
57
+ Returns:
58
+ TUIContext instance with all services initialized.
59
+ """
60
+ terminal = await TerminalCapabilities.detect(detect_osc11=detect_osc11)
61
+ store = get_account_store()
62
+ settings = load_tui_settings(store=store)
63
+
64
+ env_theme = os.getenv("AIP_TUI_THEME")
65
+ env_theme = env_theme.strip() if env_theme else None
66
+ if env_theme and env_theme.lower() == "default":
67
+ env_theme = None
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
+
76
+ theme_name = env_theme or settings.theme_name
77
+ theme = ThemeManager(
78
+ terminal,
79
+ mode=settings.theme_mode,
80
+ theme=theme_name,
81
+ settings_store=store,
82
+ )
83
+ keybinds = KeybindRegistry()
84
+ toasts = ToastBus()
85
+ clipboard = ClipboardAdapter(terminal=terminal)
86
+ return cls(
87
+ terminal=terminal,
88
+ keybinds=keybinds,
89
+ theme=theme,
90
+ toasts=toasts,
91
+ clipboard=clipboard,
92
+ )