glaip-sdk 0.6.11__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.11.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.11.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.11.dist-info/RECORD +0 -159
- glaip_sdk-0.6.11.dist-info/entry_points.txt +0 -3
|
@@ -1,628 +0,0 @@
|
|
|
1
|
-
"""Textual UI for the /runs command.
|
|
2
|
-
|
|
3
|
-
This module provides a lightweight Textual application that mirrors the remote
|
|
4
|
-
run browser experience using rich widgets (DataTable, modals, footer hints).
|
|
5
|
-
|
|
6
|
-
Authors:
|
|
7
|
-
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
from collections.abc import Callable
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from typing import Any
|
|
18
|
-
|
|
19
|
-
from rich.text import Text
|
|
20
|
-
from textual.app import App, ComposeResult
|
|
21
|
-
from textual.binding import Binding
|
|
22
|
-
from textual.containers import Container, Horizontal
|
|
23
|
-
from textual.reactive import ReactiveError
|
|
24
|
-
from textual.screen import ModalScreen
|
|
25
|
-
from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
|
|
26
|
-
|
|
27
|
-
from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
RUNS_TABLE_ID = "runs"
|
|
32
|
-
RUNS_LOADING_ID = "runs-loading"
|
|
33
|
-
RUNS_TABLE_SELECTOR = f"#{RUNS_TABLE_ID}"
|
|
34
|
-
RUNS_LOADING_SELECTOR = f"#{RUNS_LOADING_ID}"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclass
|
|
38
|
-
class RemoteRunsTUICallbacks:
|
|
39
|
-
"""Callbacks invoked by the Textual UI for data operations."""
|
|
40
|
-
|
|
41
|
-
fetch_page: Callable[[int, int], Any | None]
|
|
42
|
-
fetch_detail: Callable[[str], Any | None]
|
|
43
|
-
export_run: Callable[[str, Any | None], bool]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def run_remote_runs_textual(
|
|
47
|
-
initial_page: Any,
|
|
48
|
-
cursor_idx: int,
|
|
49
|
-
callbacks: RemoteRunsTUICallbacks,
|
|
50
|
-
*,
|
|
51
|
-
agent_name: str | None = None,
|
|
52
|
-
agent_id: str | None = None,
|
|
53
|
-
) -> tuple[int, int, int]:
|
|
54
|
-
"""Launch the Textual application and return the final pagination state.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
initial_page: RunsPage instance loaded before launching the UI.
|
|
58
|
-
cursor_idx: Previously selected row index.
|
|
59
|
-
callbacks: Data provider callback bundle.
|
|
60
|
-
agent_name: Optional agent name for display purposes.
|
|
61
|
-
agent_id: Optional agent ID for display purposes.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Tuple of (page, limit, cursor_index) after the UI exits.
|
|
65
|
-
"""
|
|
66
|
-
app = RemoteRunsTextualApp(
|
|
67
|
-
initial_page,
|
|
68
|
-
cursor_idx,
|
|
69
|
-
callbacks,
|
|
70
|
-
agent_name=agent_name,
|
|
71
|
-
agent_id=agent_id,
|
|
72
|
-
)
|
|
73
|
-
app.run()
|
|
74
|
-
current_page = getattr(app, "current_page", initial_page)
|
|
75
|
-
return current_page.page, current_page.limit, app.cursor_index
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class RunDetailScreen(ModalScreen[None]):
|
|
79
|
-
"""Modal screen displaying run metadata and output timeline."""
|
|
80
|
-
|
|
81
|
-
BINDINGS = [
|
|
82
|
-
Binding("escape", "dismiss", "Close", priority=True),
|
|
83
|
-
Binding("q", "dismiss_modal", "Close", priority=True),
|
|
84
|
-
Binding("up", "scroll_up", "Up"),
|
|
85
|
-
Binding("down", "scroll_down", "Down"),
|
|
86
|
-
Binding("pageup", "page_up", "PgUp"),
|
|
87
|
-
Binding("pagedown", "page_down", "PgDn"),
|
|
88
|
-
Binding("e", "export_detail", "Export"),
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
def __init__(self, detail: Any, on_export: Callable[[Any], None] | None = None):
|
|
92
|
-
"""Initialize the run detail screen."""
|
|
93
|
-
super().__init__()
|
|
94
|
-
self.detail = detail
|
|
95
|
-
self._on_export = on_export
|
|
96
|
-
|
|
97
|
-
def compose(self) -> ComposeResult:
|
|
98
|
-
"""Render metadata and events."""
|
|
99
|
-
meta_text = Text()
|
|
100
|
-
|
|
101
|
-
def add_meta(label: str, value: Any | None, value_style: str | None = None) -> None:
|
|
102
|
-
if value in (None, ""):
|
|
103
|
-
return
|
|
104
|
-
if len(meta_text) > 0:
|
|
105
|
-
meta_text.append("\n")
|
|
106
|
-
meta_text.append(f"{label}: ", style="bold cyan")
|
|
107
|
-
meta_text.append(str(value), style=value_style)
|
|
108
|
-
|
|
109
|
-
add_meta("Run ID", self.detail.id)
|
|
110
|
-
add_meta("Agent ID", getattr(self.detail, "agent_id", "-"))
|
|
111
|
-
add_meta("Type", getattr(self.detail, "run_type", "-"), "bold yellow")
|
|
112
|
-
status_value = getattr(self.detail, "status", "-")
|
|
113
|
-
add_meta("Status", status_value, self._status_style(status_value))
|
|
114
|
-
add_meta("Started", getattr(self.detail, "started_at", None))
|
|
115
|
-
add_meta("Completed", getattr(self.detail, "completed_at", None))
|
|
116
|
-
duration = self.detail.duration_formatted() if getattr(self.detail, "duration_formatted", None) else None
|
|
117
|
-
add_meta("Duration", duration, "bold")
|
|
118
|
-
|
|
119
|
-
yield Container(
|
|
120
|
-
Static(meta_text, id="detail-meta"),
|
|
121
|
-
RichLog(id="detail-events", wrap=False),
|
|
122
|
-
)
|
|
123
|
-
yield Footer()
|
|
124
|
-
|
|
125
|
-
def on_mount(self) -> None:
|
|
126
|
-
"""Populate and focus the log."""
|
|
127
|
-
log = self.query_one("#detail-events", RichLog)
|
|
128
|
-
log.can_focus = True
|
|
129
|
-
log.write(Text("Events", style="bold"))
|
|
130
|
-
for chunk in getattr(self.detail, "output", []):
|
|
131
|
-
event_type = chunk.get("event_type", "event")
|
|
132
|
-
status = chunk.get("status", "-")
|
|
133
|
-
timestamp = chunk.get("received_at") or "-"
|
|
134
|
-
header = Text()
|
|
135
|
-
header.append(timestamp, style="cyan")
|
|
136
|
-
header.append(" ")
|
|
137
|
-
header.append(event_type, style=self._event_type_style(event_type))
|
|
138
|
-
header.append(" ")
|
|
139
|
-
header.append("[")
|
|
140
|
-
header.append(status, style=self._status_style(status))
|
|
141
|
-
header.append("]")
|
|
142
|
-
log.write(header)
|
|
143
|
-
|
|
144
|
-
payload = Text(json.dumps(chunk, indent=2, ensure_ascii=False), style="dim")
|
|
145
|
-
log.write(payload)
|
|
146
|
-
log.write(Text(""))
|
|
147
|
-
log.focus()
|
|
148
|
-
|
|
149
|
-
def _log(self) -> RichLog:
|
|
150
|
-
return self.query_one("#detail-events", RichLog)
|
|
151
|
-
|
|
152
|
-
@staticmethod
|
|
153
|
-
def _status_style(status: str | None) -> str:
|
|
154
|
-
"""Return a Rich style name for the status pill."""
|
|
155
|
-
if not status:
|
|
156
|
-
return "dim"
|
|
157
|
-
normalized = str(status).lower()
|
|
158
|
-
if normalized in {"success", "succeeded", "completed", "ok"}:
|
|
159
|
-
return "green"
|
|
160
|
-
if normalized in {"failed", "error", "errored", "cancelled"}:
|
|
161
|
-
return "red"
|
|
162
|
-
if normalized in {"running", "in_progress", "queued"}:
|
|
163
|
-
return "yellow"
|
|
164
|
-
return "cyan"
|
|
165
|
-
|
|
166
|
-
@staticmethod
|
|
167
|
-
def _event_type_style(event_type: str | None) -> str:
|
|
168
|
-
"""Return a highlight color for the event type label."""
|
|
169
|
-
if not event_type:
|
|
170
|
-
return "white"
|
|
171
|
-
normalized = str(event_type).lower()
|
|
172
|
-
if "error" in normalized or "fail" in normalized:
|
|
173
|
-
return "red"
|
|
174
|
-
if "status" in normalized:
|
|
175
|
-
return "magenta"
|
|
176
|
-
if "tool" in normalized:
|
|
177
|
-
return "yellow"
|
|
178
|
-
if "stream" in normalized:
|
|
179
|
-
return "cyan"
|
|
180
|
-
return "green"
|
|
181
|
-
|
|
182
|
-
def action_dismiss_modal(self) -> None:
|
|
183
|
-
"""Allow q binding to close the modal like Esc."""
|
|
184
|
-
self.dismiss(None)
|
|
185
|
-
|
|
186
|
-
def action_scroll_up(self) -> None:
|
|
187
|
-
"""Scroll the log view up."""
|
|
188
|
-
self._log().action_scroll_up()
|
|
189
|
-
|
|
190
|
-
def action_scroll_down(self) -> None:
|
|
191
|
-
"""Scroll the log view down."""
|
|
192
|
-
self._log().action_scroll_down()
|
|
193
|
-
|
|
194
|
-
def action_page_up(self) -> None:
|
|
195
|
-
"""Scroll the log view up one page."""
|
|
196
|
-
self._log().action_page_up()
|
|
197
|
-
|
|
198
|
-
def action_page_down(self) -> None:
|
|
199
|
-
"""Scroll the log view down one page."""
|
|
200
|
-
self._log().action_page_down()
|
|
201
|
-
|
|
202
|
-
def action_export_detail(self) -> None:
|
|
203
|
-
"""Trigger export from the detail modal."""
|
|
204
|
-
if self._on_export is None:
|
|
205
|
-
self._announce_status("Export unavailable in this terminal mode.")
|
|
206
|
-
return
|
|
207
|
-
try:
|
|
208
|
-
self._on_export(self.detail)
|
|
209
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
210
|
-
self._announce_status(f"Export failed: {exc}")
|
|
211
|
-
|
|
212
|
-
def _announce_status(self, message: str) -> None:
|
|
213
|
-
"""Send status text to the parent app when available."""
|
|
214
|
-
try:
|
|
215
|
-
app = self.app
|
|
216
|
-
except AttributeError:
|
|
217
|
-
return
|
|
218
|
-
update_status = getattr(app, "_update_status", None)
|
|
219
|
-
if callable(update_status):
|
|
220
|
-
update_status(message, append=True)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class RemoteRunsTextualApp(App[None]):
|
|
224
|
-
"""Textual application for browsing remote runs."""
|
|
225
|
-
|
|
226
|
-
CSS = f"""
|
|
227
|
-
Screen {{ layout: vertical; }}
|
|
228
|
-
#status-bar {{ height: 3; padding: 0 1; }}
|
|
229
|
-
#agent-context {{ min-width: 25; padding-right: 1; }}
|
|
230
|
-
#{RUNS_LOADING_ID} {{ width: 8; }}
|
|
231
|
-
#status {{ padding-left: 1; }}
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
BINDINGS = [
|
|
235
|
-
Binding("q", "close_view", "Quit", priority=True),
|
|
236
|
-
Binding("escape", "close_view", "Quit", show=False, priority=True),
|
|
237
|
-
Binding("left", "page_left", "Prev page", priority=True),
|
|
238
|
-
Binding("right", "page_right", "Next page", priority=True),
|
|
239
|
-
Binding("enter", "open_detail", "Select Run", priority=True),
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
def __init__(
|
|
243
|
-
self,
|
|
244
|
-
initial_page: Any,
|
|
245
|
-
cursor_idx: int,
|
|
246
|
-
callbacks: RemoteRunsTUICallbacks,
|
|
247
|
-
*,
|
|
248
|
-
agent_name: str | None = None,
|
|
249
|
-
agent_id: str | None = None,
|
|
250
|
-
):
|
|
251
|
-
"""Initialize the remote runs Textual application.
|
|
252
|
-
|
|
253
|
-
Args:
|
|
254
|
-
initial_page: RunsPage instance to display initially.
|
|
255
|
-
cursor_idx: Initial cursor position in the table.
|
|
256
|
-
callbacks: Callback bundle for data operations.
|
|
257
|
-
agent_name: Optional agent name for display purposes.
|
|
258
|
-
agent_id: Optional agent ID for display purposes.
|
|
259
|
-
"""
|
|
260
|
-
super().__init__()
|
|
261
|
-
self.current_page = initial_page
|
|
262
|
-
self.cursor_index = max(0, min(cursor_idx, max(len(initial_page.data) - 1, 0)))
|
|
263
|
-
self.callbacks = callbacks
|
|
264
|
-
self.status_text = ""
|
|
265
|
-
self.current_rows = initial_page.data[:]
|
|
266
|
-
self.agent_name = (agent_name or "").strip()
|
|
267
|
-
self.agent_id = (agent_id or "").strip()
|
|
268
|
-
self._active_export_tasks: set[asyncio.Task[None]] = set()
|
|
269
|
-
self._page_loader_task: asyncio.Task[Any] | None = None
|
|
270
|
-
self._detail_loader_task: asyncio.Task[Any] | None = None
|
|
271
|
-
self._table_spinner_active = False
|
|
272
|
-
|
|
273
|
-
def compose(self) -> ComposeResult:
|
|
274
|
-
"""Build layout."""
|
|
275
|
-
yield Header()
|
|
276
|
-
table = DataTable(id=RUNS_TABLE_ID)
|
|
277
|
-
table.cursor_type = "row"
|
|
278
|
-
table.add_columns(
|
|
279
|
-
"Run UUID",
|
|
280
|
-
"Type",
|
|
281
|
-
"Status",
|
|
282
|
-
"Started (UTC)",
|
|
283
|
-
"Completed (UTC)",
|
|
284
|
-
"Duration",
|
|
285
|
-
"Input Preview",
|
|
286
|
-
)
|
|
287
|
-
yield table # pragma: no cover - interactive UI, tested via integration
|
|
288
|
-
yield Horizontal( # pragma: no cover - interactive UI, tested via integration
|
|
289
|
-
LoadingIndicator(id=RUNS_LOADING_ID),
|
|
290
|
-
Static(id="status"),
|
|
291
|
-
id="status-bar",
|
|
292
|
-
)
|
|
293
|
-
yield Footer() # pragma: no cover - interactive UI, tested via integration
|
|
294
|
-
|
|
295
|
-
def on_mount(self) -> None:
|
|
296
|
-
"""Render the initial page."""
|
|
297
|
-
self._hide_loading()
|
|
298
|
-
self._render_page(self.current_page)
|
|
299
|
-
|
|
300
|
-
def _render_page(self, runs_page: Any) -> None:
|
|
301
|
-
"""Populate table rows for a RunsPage."""
|
|
302
|
-
table = self.query_one(RUNS_TABLE_SELECTOR, DataTable)
|
|
303
|
-
table.clear()
|
|
304
|
-
self.current_rows = runs_page.data[:]
|
|
305
|
-
for run in self.current_rows:
|
|
306
|
-
table.add_row(
|
|
307
|
-
str(run.id),
|
|
308
|
-
str(run.run_type).title(),
|
|
309
|
-
str(run.status).upper(),
|
|
310
|
-
run.started_at.strftime("%Y-%m-%d %H:%M:%S") if run.started_at else "—",
|
|
311
|
-
run.completed_at.strftime("%Y-%m-%d %H:%M:%S") if run.completed_at else "—",
|
|
312
|
-
run.duration_formatted(),
|
|
313
|
-
run.input_preview(),
|
|
314
|
-
)
|
|
315
|
-
if self.current_rows:
|
|
316
|
-
self.cursor_index = max(0, min(self.cursor_index, len(self.current_rows) - 1))
|
|
317
|
-
table.focus()
|
|
318
|
-
table.cursor_coordinate = (self.cursor_index, 0)
|
|
319
|
-
self.current_page = runs_page
|
|
320
|
-
total_pages = max(1, (runs_page.total + runs_page.limit - 1) // runs_page.limit)
|
|
321
|
-
agent_display = self.agent_name or "Runs"
|
|
322
|
-
header = f"{agent_display} • Page {runs_page.page}/{total_pages} • Page size={runs_page.limit}"
|
|
323
|
-
try:
|
|
324
|
-
self.sub_title = header
|
|
325
|
-
except ReactiveError:
|
|
326
|
-
# App not fully initialized (common in tests), skip setting sub_title
|
|
327
|
-
logger.debug("Cannot set sub_title: app not fully initialized")
|
|
328
|
-
self._clear_status()
|
|
329
|
-
|
|
330
|
-
def _agent_context_label(self) -> str:
|
|
331
|
-
"""Return a descriptive label for the active agent."""
|
|
332
|
-
name = self.agent_name
|
|
333
|
-
identifier = self.agent_id
|
|
334
|
-
if name and identifier:
|
|
335
|
-
return f"Agent: {name} ({identifier})"
|
|
336
|
-
if name:
|
|
337
|
-
return f"Agent: {name}"
|
|
338
|
-
if identifier:
|
|
339
|
-
return f"Agent: {identifier}"
|
|
340
|
-
return "Agent runs"
|
|
341
|
-
|
|
342
|
-
def _update_status(self, message: str, *, append: bool = False) -> None:
|
|
343
|
-
"""Update the footer status text."""
|
|
344
|
-
try:
|
|
345
|
-
static = self.query_one("#status", Static)
|
|
346
|
-
except (AttributeError, RuntimeError) as e:
|
|
347
|
-
# App not fully initialized (common in tests), just update status_text
|
|
348
|
-
logger.debug("Cannot update status widget: app not fully initialized (%s)", type(e).__name__)
|
|
349
|
-
if append:
|
|
350
|
-
self.status_text = f"{self.status_text}\n{message}"
|
|
351
|
-
else:
|
|
352
|
-
self.status_text = message
|
|
353
|
-
return
|
|
354
|
-
if append:
|
|
355
|
-
self.status_text = f"{self.status_text}\n{message}"
|
|
356
|
-
else:
|
|
357
|
-
self.status_text = message
|
|
358
|
-
static.update(self.status_text)
|
|
359
|
-
|
|
360
|
-
def _clear_status(self) -> None:
|
|
361
|
-
"""Clear any status message."""
|
|
362
|
-
self.status_text = ""
|
|
363
|
-
try:
|
|
364
|
-
static = self.query_one("#status", Static)
|
|
365
|
-
static.update("")
|
|
366
|
-
except (AttributeError, RuntimeError) as e:
|
|
367
|
-
# App not fully initialized (common in tests), skip widget update
|
|
368
|
-
logger.debug("Cannot clear status widget: app not fully initialized (%s)", type(e).__name__)
|
|
369
|
-
|
|
370
|
-
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: # pragma: no cover - UI hook
|
|
371
|
-
"""Track cursor position when DataTable selection changes."""
|
|
372
|
-
self.cursor_index = getattr(event, "cursor_row", self.cursor_index)
|
|
373
|
-
|
|
374
|
-
def action_page_left(self) -> None:
|
|
375
|
-
"""Navigate to the previous page."""
|
|
376
|
-
if not self.current_page.has_prev:
|
|
377
|
-
self._update_status("Already at the first page.", append=True)
|
|
378
|
-
return
|
|
379
|
-
target_page = max(1, self.current_page.page - 1)
|
|
380
|
-
self._queue_page_load(
|
|
381
|
-
target_page,
|
|
382
|
-
loading_message="Loading previous page…",
|
|
383
|
-
failure_message="Failed to load previous page.",
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
def action_page_right(self) -> None:
|
|
387
|
-
"""Navigate to the next page."""
|
|
388
|
-
if not self.current_page.has_next:
|
|
389
|
-
self._update_status("This is the last page.", append=True)
|
|
390
|
-
return
|
|
391
|
-
target_page = self.current_page.page + 1
|
|
392
|
-
self._queue_page_load(
|
|
393
|
-
target_page,
|
|
394
|
-
loading_message="Loading next page…",
|
|
395
|
-
failure_message="Failed to load next page.",
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
def _selected_run(self) -> Any | None:
|
|
399
|
-
"""Return the currently highlighted run."""
|
|
400
|
-
if not self.current_rows:
|
|
401
|
-
return None
|
|
402
|
-
if self.cursor_index < 0 or self.cursor_index >= len(self.current_rows):
|
|
403
|
-
return None
|
|
404
|
-
return self.current_rows[self.cursor_index]
|
|
405
|
-
|
|
406
|
-
def action_open_detail(self) -> None:
|
|
407
|
-
"""Open detail modal for the selected run."""
|
|
408
|
-
run = self._selected_run()
|
|
409
|
-
if not run:
|
|
410
|
-
self._update_status("No run selected.", append=True)
|
|
411
|
-
return
|
|
412
|
-
if self._detail_loader_task and not self._detail_loader_task.done():
|
|
413
|
-
self._update_status("Already loading run detail. Please wait…", append=True)
|
|
414
|
-
return
|
|
415
|
-
run_id = str(run.id)
|
|
416
|
-
self._show_loading("Loading run detail…", table_spinner=False)
|
|
417
|
-
self._queue_detail_load(run_id)
|
|
418
|
-
|
|
419
|
-
async def action_export_run(self) -> None:
|
|
420
|
-
"""Export the selected run via callback."""
|
|
421
|
-
run = self._selected_run()
|
|
422
|
-
if not run:
|
|
423
|
-
self._update_status("No run selected.", append=True)
|
|
424
|
-
return
|
|
425
|
-
detail = self.callbacks.fetch_detail(str(run.id))
|
|
426
|
-
if detail is None:
|
|
427
|
-
self._update_status("Failed to load run detail for export.", append=True)
|
|
428
|
-
return
|
|
429
|
-
self._queue_export_job(str(run.id), detail)
|
|
430
|
-
|
|
431
|
-
def action_close_view(self) -> None:
|
|
432
|
-
"""Handle quit bindings by closing detail views first, otherwise exiting."""
|
|
433
|
-
try:
|
|
434
|
-
if isinstance(self.screen, RunDetailScreen):
|
|
435
|
-
self.pop_screen()
|
|
436
|
-
self._clear_status()
|
|
437
|
-
return
|
|
438
|
-
except (AttributeError, RuntimeError) as e:
|
|
439
|
-
# App not fully initialized (common in tests), skip screen check
|
|
440
|
-
logger.debug("Cannot check screen state: app not fully initialized (%s)", type(e).__name__)
|
|
441
|
-
self.exit()
|
|
442
|
-
|
|
443
|
-
def _queue_page_load(self, target_page: int, *, loading_message: str, failure_message: str) -> None:
|
|
444
|
-
"""Show a loading indicator and fetch a page after the next refresh."""
|
|
445
|
-
limit = self.current_page.limit
|
|
446
|
-
self._show_loading(loading_message, footer_message=False)
|
|
447
|
-
|
|
448
|
-
if self._page_loader_task and not self._page_loader_task.done():
|
|
449
|
-
self._update_status("Already loading a page. Please wait…", append=True)
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
loader_coro = self._load_page_async(target_page, limit, failure_message)
|
|
453
|
-
try:
|
|
454
|
-
task = asyncio.create_task(loader_coro, name="remote-runs-fetch")
|
|
455
|
-
except RuntimeError:
|
|
456
|
-
logger.debug("No running event loop; loading page synchronously.")
|
|
457
|
-
loader_coro.close()
|
|
458
|
-
self._load_page_sync(target_page, limit, failure_message)
|
|
459
|
-
return
|
|
460
|
-
except Exception:
|
|
461
|
-
loader_coro.close()
|
|
462
|
-
raise
|
|
463
|
-
task.add_done_callback(self._on_page_loader_done)
|
|
464
|
-
self._page_loader_task = task
|
|
465
|
-
|
|
466
|
-
def _queue_detail_load(self, run_id: str) -> None:
|
|
467
|
-
"""Fetch run detail asynchronously with spinner feedback."""
|
|
468
|
-
loader_coro = self._load_detail_async(run_id)
|
|
469
|
-
try:
|
|
470
|
-
task = asyncio.create_task(loader_coro, name=f"remote-runs-detail-{run_id}")
|
|
471
|
-
except RuntimeError:
|
|
472
|
-
logger.debug("No running event loop; loading run detail synchronously.")
|
|
473
|
-
loader_coro.close()
|
|
474
|
-
self._load_detail_sync(run_id)
|
|
475
|
-
return
|
|
476
|
-
except Exception:
|
|
477
|
-
loader_coro.close()
|
|
478
|
-
raise
|
|
479
|
-
task.add_done_callback(self._on_detail_loader_done)
|
|
480
|
-
self._detail_loader_task = task
|
|
481
|
-
|
|
482
|
-
async def _load_page_async(self, page: int, limit: int, failure_message: str) -> None:
|
|
483
|
-
"""Fetch the requested page in the background to keep the UI responsive."""
|
|
484
|
-
try:
|
|
485
|
-
new_page = await asyncio.to_thread(self.callbacks.fetch_page, page, limit)
|
|
486
|
-
except Exception as exc: # pragma: no cover - defensive logging for unexpected errors
|
|
487
|
-
logger.exception("Failed to fetch remote runs page %s: %s", page, exc)
|
|
488
|
-
new_page = None
|
|
489
|
-
finally:
|
|
490
|
-
self._hide_loading()
|
|
491
|
-
|
|
492
|
-
if new_page is None:
|
|
493
|
-
self._update_status(failure_message)
|
|
494
|
-
return
|
|
495
|
-
self._render_page(new_page)
|
|
496
|
-
|
|
497
|
-
def _load_page_sync(self, page: int, limit: int, failure_message: str) -> None:
|
|
498
|
-
"""Fallback for fetching a page when asyncio isn't active (tests)."""
|
|
499
|
-
try:
|
|
500
|
-
new_page = self.callbacks.fetch_page(page, limit)
|
|
501
|
-
except Exception as exc: # pragma: no cover - defensive logging for unexpected errors
|
|
502
|
-
logger.exception("Failed to fetch remote runs page %s: %s", page, exc)
|
|
503
|
-
new_page = None
|
|
504
|
-
finally:
|
|
505
|
-
self._hide_loading()
|
|
506
|
-
|
|
507
|
-
if new_page is None:
|
|
508
|
-
self._update_status(failure_message)
|
|
509
|
-
return
|
|
510
|
-
self._render_page(new_page)
|
|
511
|
-
|
|
512
|
-
def _on_page_loader_done(self, task: asyncio.Task[Any]) -> None:
|
|
513
|
-
"""Reset loader state and surface unexpected failures."""
|
|
514
|
-
self._page_loader_task = None
|
|
515
|
-
if task.cancelled():
|
|
516
|
-
return
|
|
517
|
-
exc = task.exception()
|
|
518
|
-
if exc:
|
|
519
|
-
logger.debug("Page loader encountered an error: %s", exc)
|
|
520
|
-
|
|
521
|
-
def _on_detail_loader_done(self, task: asyncio.Task[Any]) -> None:
|
|
522
|
-
"""Reset state for the detail fetch task."""
|
|
523
|
-
self._detail_loader_task = None
|
|
524
|
-
if task.cancelled():
|
|
525
|
-
return
|
|
526
|
-
exc = task.exception()
|
|
527
|
-
if exc:
|
|
528
|
-
logger.debug("Detail loader encountered an error: %s", exc)
|
|
529
|
-
|
|
530
|
-
async def _load_detail_async(self, run_id: str) -> None:
|
|
531
|
-
"""Retrieve run detail via background thread."""
|
|
532
|
-
try:
|
|
533
|
-
detail = await asyncio.to_thread(self.callbacks.fetch_detail, run_id)
|
|
534
|
-
except Exception as exc: # pragma: no cover - defensive logging for unexpected errors
|
|
535
|
-
logger.exception("Failed to load run detail %s: %s", run_id, exc)
|
|
536
|
-
detail = None
|
|
537
|
-
finally:
|
|
538
|
-
self._hide_loading()
|
|
539
|
-
self._present_run_detail(detail)
|
|
540
|
-
|
|
541
|
-
def _load_detail_sync(self, run_id: str) -> None:
|
|
542
|
-
"""Synchronous fallback for fetching run detail."""
|
|
543
|
-
try:
|
|
544
|
-
detail = self.callbacks.fetch_detail(run_id)
|
|
545
|
-
except Exception as exc: # pragma: no cover - defensive logging for unexpected errors
|
|
546
|
-
logger.exception("Failed to load run detail %s: %s", run_id, exc)
|
|
547
|
-
detail = None
|
|
548
|
-
finally:
|
|
549
|
-
self._hide_loading()
|
|
550
|
-
self._present_run_detail(detail)
|
|
551
|
-
|
|
552
|
-
def _present_run_detail(self, detail: Any | None) -> None:
|
|
553
|
-
"""Push the detail modal or surface an error."""
|
|
554
|
-
if detail is None:
|
|
555
|
-
self._update_status("Failed to load run detail.", append=True)
|
|
556
|
-
return
|
|
557
|
-
self.push_screen(RunDetailScreen(detail, on_export=self.queue_export_from_detail))
|
|
558
|
-
self._update_status("Detail view: ↑/↓ scroll · PgUp/PgDn · q/Esc close · e export")
|
|
559
|
-
|
|
560
|
-
def queue_export_from_detail(self, detail: Any) -> None:
|
|
561
|
-
"""Start an export from the detail modal."""
|
|
562
|
-
run_id = getattr(detail, "id", None)
|
|
563
|
-
if not run_id:
|
|
564
|
-
self._update_status("Cannot export run without an identifier.", append=True)
|
|
565
|
-
return
|
|
566
|
-
self._queue_export_job(str(run_id), detail)
|
|
567
|
-
|
|
568
|
-
def _queue_export_job(self, run_id: str, detail: Any) -> None:
|
|
569
|
-
"""Schedule the export coroutine so it can suspend cleanly."""
|
|
570
|
-
|
|
571
|
-
async def runner() -> None:
|
|
572
|
-
await self._perform_export(run_id, detail)
|
|
573
|
-
|
|
574
|
-
try:
|
|
575
|
-
self.run_worker(runner(), name="export-run", exclusive=True)
|
|
576
|
-
except Exception:
|
|
577
|
-
# Store task to prevent premature garbage collection
|
|
578
|
-
export_task = asyncio.create_task(runner())
|
|
579
|
-
# Keep reference to prevent GC (task will complete on its own)
|
|
580
|
-
self._active_export_tasks.add(export_task)
|
|
581
|
-
export_task.add_done_callback(self._active_export_tasks.discard)
|
|
582
|
-
|
|
583
|
-
async def _perform_export(self, run_id: str, detail: Any) -> None:
|
|
584
|
-
"""Execute the export callback with suspend mode."""
|
|
585
|
-
try:
|
|
586
|
-
with self.suspend():
|
|
587
|
-
success = bool(self.callbacks.export_run(run_id, detail))
|
|
588
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
589
|
-
logger.exception("Export failed: %s", exc)
|
|
590
|
-
self._update_status(f"Export failed: {exc}", append=True)
|
|
591
|
-
return
|
|
592
|
-
|
|
593
|
-
if success:
|
|
594
|
-
self._update_status("Export complete (see slash console for path).", append=True)
|
|
595
|
-
else:
|
|
596
|
-
self._update_status("Export cancelled.", append=True)
|
|
597
|
-
|
|
598
|
-
def _show_loading(
|
|
599
|
-
self,
|
|
600
|
-
message: str | None = None,
|
|
601
|
-
*,
|
|
602
|
-
table_spinner: bool = True,
|
|
603
|
-
footer_message: bool = True,
|
|
604
|
-
) -> None:
|
|
605
|
-
"""Display the loading indicator with an optional status message."""
|
|
606
|
-
show_loading_indicator(
|
|
607
|
-
self,
|
|
608
|
-
RUNS_LOADING_SELECTOR,
|
|
609
|
-
message=message if footer_message else None,
|
|
610
|
-
set_status=self._update_status if footer_message else None,
|
|
611
|
-
)
|
|
612
|
-
self._set_table_loading(table_spinner)
|
|
613
|
-
self._table_spinner_active = table_spinner
|
|
614
|
-
|
|
615
|
-
def _hide_loading(self) -> None:
|
|
616
|
-
"""Hide the loading indicator."""
|
|
617
|
-
hide_loading_indicator(self, RUNS_LOADING_SELECTOR)
|
|
618
|
-
if self._table_spinner_active:
|
|
619
|
-
self._set_table_loading(False)
|
|
620
|
-
self._table_spinner_active = False
|
|
621
|
-
|
|
622
|
-
def _set_table_loading(self, is_loading: bool) -> None:
|
|
623
|
-
"""Toggle the DataTable loading shimmer."""
|
|
624
|
-
try:
|
|
625
|
-
table = self.query_one(RUNS_TABLE_SELECTOR, DataTable)
|
|
626
|
-
table.loading = is_loading
|
|
627
|
-
except (AttributeError, RuntimeError) as e:
|
|
628
|
-
logger.debug("Cannot toggle table loading state: %s", type(e).__name__)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Transcript utilities package for CLI.
|
|
2
|
-
|
|
3
|
-
Authors:
|
|
4
|
-
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from glaip_sdk.cli.transcript.cache import (
|
|
8
|
-
export_transcript as export_cached_transcript,
|
|
9
|
-
)
|
|
10
|
-
from glaip_sdk.cli.transcript.cache import (
|
|
11
|
-
get_transcript_cache_stats,
|
|
12
|
-
suggest_filename,
|
|
13
|
-
)
|
|
14
|
-
from glaip_sdk.cli.transcript.capture import store_transcript_for_session
|
|
15
|
-
from glaip_sdk.cli.transcript.export import (
|
|
16
|
-
normalise_export_destination,
|
|
17
|
-
resolve_manifest_for_export,
|
|
18
|
-
)
|
|
19
|
-
from glaip_sdk.cli.transcript.history import load_history_snapshot
|
|
20
|
-
from glaip_sdk.cli.transcript.launcher import maybe_launch_post_run_viewer
|
|
21
|
-
|
|
22
|
-
__all__ = [
|
|
23
|
-
"export_cached_transcript",
|
|
24
|
-
"get_transcript_cache_stats",
|
|
25
|
-
"load_history_snapshot",
|
|
26
|
-
"maybe_launch_post_run_viewer",
|
|
27
|
-
"normalise_export_destination",
|
|
28
|
-
"resolve_manifest_for_export",
|
|
29
|
-
"store_transcript_for_session",
|
|
30
|
-
"suggest_filename",
|
|
31
|
-
]
|