glaip-sdk 0.6.12__py3-none-any.whl → 0.6.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.
Files changed (156) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
  3. glaip_sdk-0.6.14.dist-info/RECORD +12 -0
  4. {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
  5. glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
  6. glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
  7. glaip_sdk/agents/__init__.py +0 -27
  8. glaip_sdk/agents/base.py +0 -1191
  9. glaip_sdk/cli/__init__.py +0 -9
  10. glaip_sdk/cli/account_store.py +0 -540
  11. glaip_sdk/cli/agent_config.py +0 -78
  12. glaip_sdk/cli/auth.py +0 -699
  13. glaip_sdk/cli/commands/__init__.py +0 -5
  14. glaip_sdk/cli/commands/accounts.py +0 -746
  15. glaip_sdk/cli/commands/agents.py +0 -1509
  16. glaip_sdk/cli/commands/common_config.py +0 -101
  17. glaip_sdk/cli/commands/configure.py +0 -896
  18. glaip_sdk/cli/commands/mcps.py +0 -1356
  19. glaip_sdk/cli/commands/models.py +0 -69
  20. glaip_sdk/cli/commands/tools.py +0 -576
  21. glaip_sdk/cli/commands/transcripts.py +0 -755
  22. glaip_sdk/cli/commands/update.py +0 -61
  23. glaip_sdk/cli/config.py +0 -95
  24. glaip_sdk/cli/constants.py +0 -38
  25. glaip_sdk/cli/context.py +0 -150
  26. glaip_sdk/cli/core/__init__.py +0 -79
  27. glaip_sdk/cli/core/context.py +0 -124
  28. glaip_sdk/cli/core/output.py +0 -846
  29. glaip_sdk/cli/core/prompting.py +0 -649
  30. glaip_sdk/cli/core/rendering.py +0 -187
  31. glaip_sdk/cli/display.py +0 -355
  32. glaip_sdk/cli/hints.py +0 -57
  33. glaip_sdk/cli/io.py +0 -112
  34. glaip_sdk/cli/main.py +0 -604
  35. glaip_sdk/cli/masking.py +0 -136
  36. glaip_sdk/cli/mcp_validators.py +0 -287
  37. glaip_sdk/cli/pager.py +0 -266
  38. glaip_sdk/cli/parsers/__init__.py +0 -7
  39. glaip_sdk/cli/parsers/json_input.py +0 -177
  40. glaip_sdk/cli/resolution.py +0 -67
  41. glaip_sdk/cli/rich_helpers.py +0 -27
  42. glaip_sdk/cli/slash/__init__.py +0 -15
  43. glaip_sdk/cli/slash/accounts_controller.py +0 -578
  44. glaip_sdk/cli/slash/accounts_shared.py +0 -75
  45. glaip_sdk/cli/slash/agent_session.py +0 -285
  46. glaip_sdk/cli/slash/prompt.py +0 -256
  47. glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
  48. glaip_sdk/cli/slash/session.py +0 -1708
  49. glaip_sdk/cli/slash/tui/__init__.py +0 -9
  50. glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
  51. glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
  52. glaip_sdk/cli/slash/tui/loading.py +0 -58
  53. glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
  54. glaip_sdk/cli/transcript/__init__.py +0 -31
  55. glaip_sdk/cli/transcript/cache.py +0 -536
  56. glaip_sdk/cli/transcript/capture.py +0 -329
  57. glaip_sdk/cli/transcript/export.py +0 -38
  58. glaip_sdk/cli/transcript/history.py +0 -815
  59. glaip_sdk/cli/transcript/launcher.py +0 -77
  60. glaip_sdk/cli/transcript/viewer.py +0 -374
  61. glaip_sdk/cli/update_notifier.py +0 -290
  62. glaip_sdk/cli/utils.py +0 -263
  63. glaip_sdk/cli/validators.py +0 -238
  64. glaip_sdk/client/__init__.py +0 -11
  65. glaip_sdk/client/_agent_payloads.py +0 -520
  66. glaip_sdk/client/agent_runs.py +0 -147
  67. glaip_sdk/client/agents.py +0 -1335
  68. glaip_sdk/client/base.py +0 -502
  69. glaip_sdk/client/main.py +0 -249
  70. glaip_sdk/client/mcps.py +0 -370
  71. glaip_sdk/client/run_rendering.py +0 -700
  72. glaip_sdk/client/shared.py +0 -21
  73. glaip_sdk/client/tools.py +0 -661
  74. glaip_sdk/client/validators.py +0 -198
  75. glaip_sdk/config/constants.py +0 -52
  76. glaip_sdk/mcps/__init__.py +0 -21
  77. glaip_sdk/mcps/base.py +0 -345
  78. glaip_sdk/models/__init__.py +0 -90
  79. glaip_sdk/models/agent.py +0 -47
  80. glaip_sdk/models/agent_runs.py +0 -116
  81. glaip_sdk/models/common.py +0 -42
  82. glaip_sdk/models/mcp.py +0 -33
  83. glaip_sdk/models/tool.py +0 -33
  84. glaip_sdk/payload_schemas/__init__.py +0 -7
  85. glaip_sdk/payload_schemas/agent.py +0 -85
  86. glaip_sdk/registry/__init__.py +0 -55
  87. glaip_sdk/registry/agent.py +0 -164
  88. glaip_sdk/registry/base.py +0 -139
  89. glaip_sdk/registry/mcp.py +0 -253
  90. glaip_sdk/registry/tool.py +0 -232
  91. glaip_sdk/runner/__init__.py +0 -59
  92. glaip_sdk/runner/base.py +0 -84
  93. glaip_sdk/runner/deps.py +0 -115
  94. glaip_sdk/runner/langgraph.py +0 -782
  95. glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
  99. glaip_sdk/runner/tool_adapter/__init__.py +0 -18
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
  102. glaip_sdk/tools/__init__.py +0 -22
  103. glaip_sdk/tools/base.py +0 -435
  104. glaip_sdk/utils/__init__.py +0 -86
  105. glaip_sdk/utils/a2a/__init__.py +0 -34
  106. glaip_sdk/utils/a2a/event_processor.py +0 -188
  107. glaip_sdk/utils/agent_config.py +0 -194
  108. glaip_sdk/utils/bundler.py +0 -267
  109. glaip_sdk/utils/client.py +0 -111
  110. glaip_sdk/utils/client_utils.py +0 -486
  111. glaip_sdk/utils/datetime_helpers.py +0 -58
  112. glaip_sdk/utils/discovery.py +0 -78
  113. glaip_sdk/utils/display.py +0 -135
  114. glaip_sdk/utils/export.py +0 -143
  115. glaip_sdk/utils/general.py +0 -61
  116. glaip_sdk/utils/import_export.py +0 -168
  117. glaip_sdk/utils/import_resolver.py +0 -492
  118. glaip_sdk/utils/instructions.py +0 -101
  119. glaip_sdk/utils/rendering/__init__.py +0 -115
  120. glaip_sdk/utils/rendering/formatting.py +0 -264
  121. glaip_sdk/utils/rendering/layout/__init__.py +0 -64
  122. glaip_sdk/utils/rendering/layout/panels.py +0 -156
  123. glaip_sdk/utils/rendering/layout/progress.py +0 -202
  124. glaip_sdk/utils/rendering/layout/summary.py +0 -74
  125. glaip_sdk/utils/rendering/layout/transcript.py +0 -606
  126. glaip_sdk/utils/rendering/models.py +0 -85
  127. glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
  128. glaip_sdk/utils/rendering/renderer/base.py +0 -1024
  129. glaip_sdk/utils/rendering/renderer/config.py +0 -27
  130. glaip_sdk/utils/rendering/renderer/console.py +0 -55
  131. glaip_sdk/utils/rendering/renderer/debug.py +0 -178
  132. glaip_sdk/utils/rendering/renderer/factory.py +0 -138
  133. glaip_sdk/utils/rendering/renderer/stream.py +0 -202
  134. glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
  135. glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
  136. glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
  137. glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
  138. glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
  139. glaip_sdk/utils/rendering/state.py +0 -204
  140. glaip_sdk/utils/rendering/step_tree_state.py +0 -100
  141. glaip_sdk/utils/rendering/steps/__init__.py +0 -34
  142. glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
  143. glaip_sdk/utils/rendering/steps/format.py +0 -176
  144. glaip_sdk/utils/rendering/steps/manager.py +0 -387
  145. glaip_sdk/utils/rendering/timing.py +0 -36
  146. glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
  147. glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
  148. glaip_sdk/utils/resource_refs.py +0 -195
  149. glaip_sdk/utils/run_renderer.py +0 -41
  150. glaip_sdk/utils/runtime_config.py +0 -425
  151. glaip_sdk/utils/serialization.py +0 -424
  152. glaip_sdk/utils/sync.py +0 -142
  153. glaip_sdk/utils/tool_detection.py +0 -33
  154. glaip_sdk/utils/validation.py +0 -264
  155. glaip_sdk-0.6.12.dist-info/RECORD +0 -159
  156. glaip_sdk-0.6.12.dist-info/entry_points.txt +0 -3
@@ -1,273 +0,0 @@
1
- """Thinking scope controller used by the renderer.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from time import monotonic
10
- from typing import Any
11
-
12
- from glaip_sdk.utils.rendering.formatting import is_step_finished
13
- from glaip_sdk.utils.rendering.models import Step
14
- from glaip_sdk.utils.rendering.state import ThinkingScopeState
15
- from glaip_sdk.utils.rendering.steps import StepManager
16
- from glaip_sdk.utils.rendering.timing import calculate_timeline_duration, coerce_server_time
17
-
18
- FINISHED_STATUS_HINTS = {
19
- "finished",
20
- "success",
21
- "succeeded",
22
- "completed",
23
- "failed",
24
- "stopped",
25
- "error",
26
- }
27
-
28
-
29
- class ThinkingScopeController:
30
- """Encapsulates deterministic thinking bookkeeping for the renderer."""
31
-
32
- def __init__(self, steps: StepManager, *, step_server_start_times: dict[str, float]) -> None:
33
- """Initialize the thinking scope controller.
34
-
35
- Args:
36
- steps: Step manager instance for tracking steps
37
- step_server_start_times: Dictionary mapping step IDs to server start times
38
- """
39
- self._steps = steps
40
- self._step_server_start_times = step_server_start_times
41
- self._scopes: dict[str, ThinkingScopeState] = {}
42
-
43
- def update_timeline(self, step: Step | None, payload: dict[str, Any], *, enabled: bool) -> None:
44
- """Update thinking spans for a streamed step event."""
45
- if not enabled or not step:
46
- return
47
-
48
- now_monotonic = monotonic()
49
- server_time = coerce_server_time(payload.get("time"))
50
- status_hint = (payload.get("status") or "").lower()
51
-
52
- if self._is_scope_anchor(step):
53
- self._update_anchor_thinking(
54
- step=step,
55
- server_time=server_time,
56
- status_hint=status_hint,
57
- now_monotonic=now_monotonic,
58
- )
59
- return
60
-
61
- self._update_child_thinking(
62
- step=step,
63
- server_time=server_time,
64
- status_hint=status_hint,
65
- now_monotonic=now_monotonic,
66
- )
67
-
68
- def close_active_scopes(self, server_time: float | None) -> None:
69
- """Finish any in-flight thinking nodes during finalization."""
70
- now = monotonic()
71
- for scope in self._scopes.values():
72
- if not scope.active_thinking_id:
73
- continue
74
- self._finish_scope_thinking(scope, server_time, now)
75
-
76
- # ------------------------------------------------------------------
77
- # Internal helpers mirroring the previous renderer implementation.
78
- # ------------------------------------------------------------------
79
- def _update_anchor_thinking(
80
- self,
81
- *,
82
- step: Step,
83
- server_time: float | None,
84
- status_hint: str,
85
- now_monotonic: float,
86
- ) -> None:
87
- scope = self._get_or_create_scope(step)
88
- if scope.anchor_started_at is None and server_time is not None:
89
- scope.anchor_started_at = server_time
90
-
91
- if not scope.closed and scope.active_thinking_id is None:
92
- self._start_scope_thinking(
93
- scope,
94
- start_server_time=scope.anchor_started_at or server_time,
95
- start_monotonic=now_monotonic,
96
- )
97
-
98
- is_anchor_finished = status_hint in FINISHED_STATUS_HINTS or (not status_hint and is_step_finished(step))
99
- if is_anchor_finished:
100
- scope.anchor_finished_at = server_time or scope.anchor_finished_at
101
- self._finish_scope_thinking(scope, server_time, now_monotonic)
102
- scope.closed = True
103
-
104
- parent_anchor_id = self._resolve_anchor_id(step)
105
- if parent_anchor_id:
106
- self._cascade_anchor_update(
107
- parent_anchor_id=parent_anchor_id,
108
- child_step=step,
109
- server_time=server_time,
110
- now_monotonic=now_monotonic,
111
- is_finished=is_anchor_finished,
112
- )
113
-
114
- def _cascade_anchor_update(
115
- self,
116
- *,
117
- parent_anchor_id: str,
118
- child_step: Step,
119
- server_time: float | None,
120
- now_monotonic: float,
121
- is_finished: bool,
122
- ) -> None:
123
- parent_scope = self._scopes.get(parent_anchor_id)
124
- if not parent_scope or parent_scope.closed:
125
- return
126
- if is_finished:
127
- self._mark_child_finished(parent_scope, child_step.step_id, server_time, now_monotonic)
128
- else:
129
- self._mark_child_running(parent_scope, child_step, server_time, now_monotonic)
130
-
131
- def _update_child_thinking(
132
- self,
133
- *,
134
- step: Step,
135
- server_time: float | None,
136
- status_hint: str,
137
- now_monotonic: float,
138
- ) -> None:
139
- anchor_id = self._resolve_anchor_id(step)
140
- if not anchor_id:
141
- return
142
-
143
- scope = self._scopes.get(anchor_id)
144
- if not scope or scope.closed or step.kind == "thinking":
145
- return
146
-
147
- is_finish_event = status_hint in FINISHED_STATUS_HINTS or (not status_hint and is_step_finished(step))
148
- if is_finish_event:
149
- self._mark_child_finished(scope, step.step_id, server_time, now_monotonic)
150
- else:
151
- self._mark_child_running(scope, step, server_time, now_monotonic)
152
-
153
- def _resolve_anchor_id(self, step: Step) -> str | None:
154
- parent_id = step.parent_id
155
- while parent_id:
156
- parent = self._steps.by_id.get(parent_id)
157
- if not parent:
158
- return None
159
- if self._is_scope_anchor(parent):
160
- return parent.step_id
161
- parent_id = parent.parent_id
162
- return None
163
-
164
- def _get_or_create_scope(self, step: Step) -> ThinkingScopeState:
165
- scope = self._scopes.get(step.step_id)
166
- if scope:
167
- if scope.task_id is None:
168
- scope.task_id = step.task_id
169
- if scope.context_id is None:
170
- scope.context_id = step.context_id
171
- return scope
172
- scope = ThinkingScopeState(
173
- anchor_id=step.step_id,
174
- task_id=step.task_id,
175
- context_id=step.context_id,
176
- )
177
- self._scopes[step.step_id] = scope
178
- return scope
179
-
180
- def _is_scope_anchor(self, step: Step) -> bool:
181
- if step.kind in {"agent", "delegate"}:
182
- return True
183
- name = (step.name or "").lower()
184
- return name.startswith(("delegate_to_", "delegate_", "delegate "))
185
-
186
- def _start_scope_thinking(
187
- self,
188
- scope: ThinkingScopeState,
189
- *,
190
- start_server_time: float | None,
191
- start_monotonic: float,
192
- ) -> None:
193
- if scope.closed or scope.active_thinking_id or not scope.anchor_id:
194
- return
195
- step = self._steps.start_or_get(
196
- task_id=scope.task_id,
197
- context_id=scope.context_id,
198
- kind="thinking",
199
- name=f"agent_thinking_step::{scope.anchor_id}",
200
- parent_id=scope.anchor_id,
201
- args={"reason": "deterministic_timeline"},
202
- )
203
- step.display_label = "💭 Thinking…"
204
- scope.active_thinking_id = step.step_id
205
- scope.idle_started_at = start_server_time
206
- scope.idle_started_monotonic = start_monotonic
207
-
208
- def _finish_scope_thinking(
209
- self,
210
- scope: ThinkingScopeState,
211
- end_server_time: float | None,
212
- end_monotonic: float,
213
- ) -> None:
214
- if not scope.active_thinking_id:
215
- return
216
- thinking_step = self._steps.by_id.get(scope.active_thinking_id)
217
- if not thinking_step:
218
- scope.active_thinking_id = None
219
- scope.idle_started_at = None
220
- scope.idle_started_monotonic = None
221
- return
222
-
223
- duration = calculate_timeline_duration(
224
- scope.idle_started_at,
225
- end_server_time,
226
- scope.idle_started_monotonic,
227
- end_monotonic,
228
- )
229
- thinking_step.display_label = thinking_step.display_label or "💭 Thinking…"
230
- if duration is not None:
231
- thinking_step.finish(duration, source="timeline")
232
- else:
233
- thinking_step.finish(None, source="timeline")
234
- scope.active_thinking_id = None
235
- scope.idle_started_at = None
236
- scope.idle_started_monotonic = None
237
-
238
- def _mark_child_running(
239
- self,
240
- scope: ThinkingScopeState,
241
- step: Step,
242
- server_time: float | None,
243
- now_monotonic: float,
244
- ) -> None:
245
- if step.step_id in scope.running_children:
246
- return
247
- scope.running_children.add(step.step_id)
248
- if not scope.active_thinking_id:
249
- return
250
-
251
- start_server = self._step_server_start_times.get(step.step_id)
252
- if start_server is None:
253
- start_server = server_time
254
- self._finish_scope_thinking(scope, start_server, now_monotonic)
255
-
256
- def _mark_child_finished(
257
- self,
258
- scope: ThinkingScopeState,
259
- step_id: str,
260
- server_time: float | None,
261
- now_monotonic: float,
262
- ) -> None:
263
- scope.running_children.discard(step_id)
264
- if scope.active_thinking_id or scope.closed or scope.running_children:
265
- return
266
- self._start_scope_thinking(
267
- scope,
268
- start_server_time=server_time,
269
- start_monotonic=now_monotonic,
270
- )
271
-
272
-
273
- __all__ = ["ThinkingScopeController", "FINISHED_STATUS_HINTS"]
@@ -1,182 +0,0 @@
1
- """Keyboard-driven transcript toggling support for the live renderer.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import os
10
- import sys
11
- import threading
12
- import time
13
- from typing import Any
14
-
15
- try: # pragma: no cover - Windows-specific dependencies
16
- import msvcrt # type: ignore[import]
17
- except ImportError: # pragma: no cover - POSIX fallback
18
- msvcrt = None # type: ignore[assignment]
19
-
20
- if os.name != "nt": # pragma: no cover - POSIX-only imports
21
- import select
22
- import termios
23
- import tty
24
-
25
-
26
- CTRL_T = "\x14"
27
-
28
-
29
- class TranscriptToggleController:
30
- """Manage mid-run transcript toggling for RichStreamRenderer instances."""
31
-
32
- def __init__(self, *, enabled: bool) -> None:
33
- """Initialise controller.
34
-
35
- Args:
36
- enabled: Whether toggling should be active (usually gated by TTY checks).
37
- """
38
- self._enabled = enabled and bool(sys.stdin) and sys.stdin.isatty()
39
- self._lock = threading.Lock()
40
- self._posix_fd: int | None = None
41
- self._posix_attrs: list[int] | None = None
42
- self._active = False
43
- self._stop_event = threading.Event()
44
- self._poll_thread: threading.Thread | None = None
45
-
46
- @property
47
- def enabled(self) -> bool:
48
- """Return True when controller is able to process keypresses."""
49
- return self._enabled
50
-
51
- def on_stream_start(self, renderer: Any) -> None:
52
- """Prepare terminal state before streaming begins."""
53
- if not self._enabled:
54
- return
55
-
56
- if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
57
- self._active = True
58
- self._start_polling_thread(renderer)
59
- return
60
-
61
- fd = sys.stdin.fileno()
62
- try:
63
- attrs = termios.tcgetattr(fd)
64
- except Exception:
65
- self._enabled = False
66
- return
67
-
68
- try:
69
- tty.setcbreak(fd)
70
- except Exception:
71
- try:
72
- termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
73
- except Exception:
74
- pass
75
- self._enabled = False
76
- return
77
-
78
- with self._lock:
79
- self._posix_fd = fd
80
- self._posix_attrs = attrs
81
- self._active = True
82
-
83
- self._start_polling_thread(renderer)
84
-
85
- def on_stream_complete(self) -> None:
86
- """Restore terminal state when streaming ends."""
87
- if not self._active:
88
- return
89
-
90
- self._stop_polling_thread()
91
-
92
- if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
93
- self._active = False
94
- return
95
-
96
- with self._lock:
97
- fd = self._posix_fd
98
- attrs = self._posix_attrs
99
- self._posix_fd = None
100
- self._posix_attrs = None
101
- self._active = False
102
-
103
- if fd is None or attrs is None:
104
- return
105
-
106
- try:
107
- termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
108
- except Exception:
109
- pass
110
-
111
- def poll(self, renderer: Any) -> None:
112
- """Poll for toggle keypresses and update renderer if needed."""
113
- if not self._active:
114
- return
115
-
116
- if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
117
- self._poll_windows(renderer)
118
- else:
119
- self._poll_posix(renderer)
120
-
121
- # ------------------------------------------------------------------
122
- # Platform-specific polling
123
- # ------------------------------------------------------------------
124
- def _poll_windows(self, renderer: Any) -> None:
125
- if not msvcrt: # pragma: no cover - safety guard
126
- return
127
-
128
- while msvcrt.kbhit():
129
- ch = msvcrt.getwch()
130
- if ch == CTRL_T:
131
- renderer.toggle_transcript_mode()
132
-
133
- def _poll_posix(self, renderer: Any) -> None: # pragma: no cover - requires TTY
134
- fd = self._posix_fd
135
- if fd is None:
136
- return
137
-
138
- while True:
139
- readable, _, _ = select.select([fd], [], [], 0)
140
- if not readable:
141
- return
142
-
143
- try:
144
- data = os.read(fd, 1)
145
- except Exception:
146
- return
147
-
148
- if not data:
149
- return
150
-
151
- ch = data.decode(errors="ignore")
152
- if ch == CTRL_T:
153
- renderer.toggle_transcript_mode()
154
-
155
- def _start_polling_thread(self, renderer: Any) -> None:
156
- if self._poll_thread and self._poll_thread.is_alive():
157
- return
158
- if not self._active:
159
- return
160
-
161
- self._stop_event.clear()
162
- self._poll_thread = threading.Thread(target=self._poll_loop, args=(renderer,), daemon=True)
163
- self._poll_thread.start()
164
-
165
- def _stop_polling_thread(self) -> None:
166
- self._stop_event.set()
167
- thread = self._poll_thread
168
- if thread and thread.is_alive():
169
- thread.join(timeout=0.2)
170
- self._poll_thread = None
171
-
172
- def _poll_loop(self, renderer: Any) -> None:
173
- while self._active and not self._stop_event.is_set():
174
- try:
175
- if os.name == "nt":
176
- self._poll_windows(renderer)
177
- else:
178
- self._poll_posix(renderer)
179
- except Exception:
180
- # Never let background polling disrupt the main stream
181
- pass
182
- time.sleep(0.05)