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.
- glaip_sdk/__init__.py +42 -5
- {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
- glaip_sdk-0.6.14.dist-info/RECORD +12 -0
- {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
- glaip_sdk/agents/__init__.py +0 -27
- glaip_sdk/agents/base.py +0 -1191
- glaip_sdk/cli/__init__.py +0 -9
- glaip_sdk/cli/account_store.py +0 -540
- glaip_sdk/cli/agent_config.py +0 -78
- glaip_sdk/cli/auth.py +0 -699
- glaip_sdk/cli/commands/__init__.py +0 -5
- glaip_sdk/cli/commands/accounts.py +0 -746
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/common_config.py +0 -101
- glaip_sdk/cli/commands/configure.py +0 -896
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/models.py +0 -69
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/commands/transcripts.py +0 -755
- glaip_sdk/cli/commands/update.py +0 -61
- glaip_sdk/cli/config.py +0 -95
- glaip_sdk/cli/constants.py +0 -38
- glaip_sdk/cli/context.py +0 -150
- glaip_sdk/cli/core/__init__.py +0 -79
- glaip_sdk/cli/core/context.py +0 -124
- glaip_sdk/cli/core/output.py +0 -846
- glaip_sdk/cli/core/prompting.py +0 -649
- glaip_sdk/cli/core/rendering.py +0 -187
- glaip_sdk/cli/display.py +0 -355
- glaip_sdk/cli/hints.py +0 -57
- glaip_sdk/cli/io.py +0 -112
- glaip_sdk/cli/main.py +0 -604
- glaip_sdk/cli/masking.py +0 -136
- glaip_sdk/cli/mcp_validators.py +0 -287
- glaip_sdk/cli/pager.py +0 -266
- glaip_sdk/cli/parsers/__init__.py +0 -7
- glaip_sdk/cli/parsers/json_input.py +0 -177
- glaip_sdk/cli/resolution.py +0 -67
- glaip_sdk/cli/rich_helpers.py +0 -27
- glaip_sdk/cli/slash/__init__.py +0 -15
- glaip_sdk/cli/slash/accounts_controller.py +0 -578
- glaip_sdk/cli/slash/accounts_shared.py +0 -75
- glaip_sdk/cli/slash/agent_session.py +0 -285
- glaip_sdk/cli/slash/prompt.py +0 -256
- glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
- glaip_sdk/cli/slash/session.py +0 -1708
- glaip_sdk/cli/slash/tui/__init__.py +0 -9
- glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
- glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
- glaip_sdk/cli/slash/tui/loading.py +0 -58
- glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
- glaip_sdk/cli/transcript/__init__.py +0 -31
- glaip_sdk/cli/transcript/cache.py +0 -536
- glaip_sdk/cli/transcript/capture.py +0 -329
- glaip_sdk/cli/transcript/export.py +0 -38
- glaip_sdk/cli/transcript/history.py +0 -815
- glaip_sdk/cli/transcript/launcher.py +0 -77
- glaip_sdk/cli/transcript/viewer.py +0 -374
- glaip_sdk/cli/update_notifier.py +0 -290
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk/cli/validators.py +0 -238
- glaip_sdk/client/__init__.py +0 -11
- glaip_sdk/client/_agent_payloads.py +0 -520
- glaip_sdk/client/agent_runs.py +0 -147
- glaip_sdk/client/agents.py +0 -1335
- glaip_sdk/client/base.py +0 -502
- glaip_sdk/client/main.py +0 -249
- glaip_sdk/client/mcps.py +0 -370
- glaip_sdk/client/run_rendering.py +0 -700
- glaip_sdk/client/shared.py +0 -21
- glaip_sdk/client/tools.py +0 -661
- glaip_sdk/client/validators.py +0 -198
- glaip_sdk/config/constants.py +0 -52
- glaip_sdk/mcps/__init__.py +0 -21
- glaip_sdk/mcps/base.py +0 -345
- glaip_sdk/models/__init__.py +0 -90
- glaip_sdk/models/agent.py +0 -47
- glaip_sdk/models/agent_runs.py +0 -116
- glaip_sdk/models/common.py +0 -42
- glaip_sdk/models/mcp.py +0 -33
- glaip_sdk/models/tool.py +0 -33
- glaip_sdk/payload_schemas/__init__.py +0 -7
- glaip_sdk/payload_schemas/agent.py +0 -85
- glaip_sdk/registry/__init__.py +0 -55
- glaip_sdk/registry/agent.py +0 -164
- glaip_sdk/registry/base.py +0 -139
- glaip_sdk/registry/mcp.py +0 -253
- glaip_sdk/registry/tool.py +0 -232
- glaip_sdk/runner/__init__.py +0 -59
- glaip_sdk/runner/base.py +0 -84
- glaip_sdk/runner/deps.py +0 -115
- glaip_sdk/runner/langgraph.py +0 -782
- glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
- glaip_sdk/runner/tool_adapter/__init__.py +0 -18
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
- glaip_sdk/tools/__init__.py +0 -22
- glaip_sdk/tools/base.py +0 -435
- glaip_sdk/utils/__init__.py +0 -86
- glaip_sdk/utils/a2a/__init__.py +0 -34
- glaip_sdk/utils/a2a/event_processor.py +0 -188
- glaip_sdk/utils/agent_config.py +0 -194
- glaip_sdk/utils/bundler.py +0 -267
- glaip_sdk/utils/client.py +0 -111
- glaip_sdk/utils/client_utils.py +0 -486
- glaip_sdk/utils/datetime_helpers.py +0 -58
- glaip_sdk/utils/discovery.py +0 -78
- glaip_sdk/utils/display.py +0 -135
- glaip_sdk/utils/export.py +0 -143
- glaip_sdk/utils/general.py +0 -61
- glaip_sdk/utils/import_export.py +0 -168
- glaip_sdk/utils/import_resolver.py +0 -492
- glaip_sdk/utils/instructions.py +0 -101
- glaip_sdk/utils/rendering/__init__.py +0 -115
- glaip_sdk/utils/rendering/formatting.py +0 -264
- glaip_sdk/utils/rendering/layout/__init__.py +0 -64
- glaip_sdk/utils/rendering/layout/panels.py +0 -156
- glaip_sdk/utils/rendering/layout/progress.py +0 -202
- glaip_sdk/utils/rendering/layout/summary.py +0 -74
- glaip_sdk/utils/rendering/layout/transcript.py +0 -606
- glaip_sdk/utils/rendering/models.py +0 -85
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
- glaip_sdk/utils/rendering/renderer/base.py +0 -1024
- glaip_sdk/utils/rendering/renderer/config.py +0 -27
- glaip_sdk/utils/rendering/renderer/console.py +0 -55
- glaip_sdk/utils/rendering/renderer/debug.py +0 -178
- glaip_sdk/utils/rendering/renderer/factory.py +0 -138
- glaip_sdk/utils/rendering/renderer/stream.py +0 -202
- glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
- glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
- glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
- glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
- glaip_sdk/utils/rendering/state.py +0 -204
- glaip_sdk/utils/rendering/step_tree_state.py +0 -100
- glaip_sdk/utils/rendering/steps/__init__.py +0 -34
- glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
- glaip_sdk/utils/rendering/steps/format.py +0 -176
- glaip_sdk/utils/rendering/steps/manager.py +0 -387
- glaip_sdk/utils/rendering/timing.py +0 -36
- glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
- glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
- glaip_sdk/utils/resource_refs.py +0 -195
- glaip_sdk/utils/run_renderer.py +0 -41
- glaip_sdk/utils/runtime_config.py +0 -425
- glaip_sdk/utils/serialization.py +0 -424
- glaip_sdk/utils/sync.py +0 -142
- glaip_sdk/utils/tool_detection.py +0 -33
- glaip_sdk/utils/validation.py +0 -264
- glaip_sdk-0.6.12.dist-info/RECORD +0 -159
- 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)
|