glaip-sdk 0.6.12__py3-none-any.whl → 0.6.15__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.15.dist-info}/METADATA +32 -37
  3. glaip_sdk-0.6.15.dist-info/RECORD +12 -0
  4. {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.15.dist-info}/WHEEL +2 -1
  5. glaip_sdk-0.6.15.dist-info/entry_points.txt +2 -0
  6. glaip_sdk-0.6.15.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,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
- ]