glaip-sdk 0.6.5b3__py3-none-any.whl → 0.7.17__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 (145) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +362 -39
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +375 -25
  55. glaip_sdk/cli/slash/tui/__init__.py +28 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +5 -3
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +57 -26
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +89 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/registry/tool.py +273 -66
  109. glaip_sdk/runner/__init__.py +76 -0
  110. glaip_sdk/runner/base.py +84 -0
  111. glaip_sdk/runner/deps.py +115 -0
  112. glaip_sdk/runner/langgraph.py +1055 -0
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  115. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  116. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  117. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  118. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  119. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  120. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  121. glaip_sdk/schedules/__init__.py +22 -0
  122. glaip_sdk/schedules/base.py +291 -0
  123. glaip_sdk/tools/base.py +67 -14
  124. glaip_sdk/utils/__init__.py +1 -0
  125. glaip_sdk/utils/a2a/__init__.py +34 -0
  126. glaip_sdk/utils/a2a/event_processor.py +188 -0
  127. glaip_sdk/utils/agent_config.py +8 -2
  128. glaip_sdk/utils/bundler.py +138 -2
  129. glaip_sdk/utils/import_resolver.py +43 -11
  130. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  131. glaip_sdk/utils/runtime_config.py +120 -0
  132. glaip_sdk/utils/sync.py +31 -11
  133. glaip_sdk/utils/tool_detection.py +301 -0
  134. glaip_sdk/utils/tool_storage_provider.py +140 -0
  135. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
  136. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  137. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  138. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  139. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  140. glaip_sdk/cli/commands/agents.py +0 -1509
  141. glaip_sdk/cli/commands/mcps.py +0 -1356
  142. glaip_sdk/cli/commands/tools.py +0 -576
  143. glaip_sdk/cli/utils.py +0 -263
  144. glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
  145. glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
@@ -17,14 +17,20 @@ from dataclasses import dataclass
17
17
  from typing import Any
18
18
 
19
19
  from rich.text import Text
20
+
20
21
  from textual.app import App, ComposeResult
21
22
  from textual.binding import Binding
22
- from textual.containers import Container, Horizontal
23
+ from textual.containers import Horizontal, Vertical
24
+ from textual.coordinate import Coordinate
23
25
  from textual.reactive import ReactiveError
24
26
  from textual.screen import ModalScreen
25
- from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
27
+ from textual.widgets import DataTable, Footer, Header, RichLog, Static
26
28
 
29
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
30
+ from glaip_sdk.cli.slash.tui.context import TUIContext
31
+ from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
27
32
  from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
33
+ from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastContainer, ToastHandlerMixin
28
34
 
29
35
  logger = logging.getLogger(__name__)
30
36
 
@@ -50,6 +56,7 @@ def run_remote_runs_textual(
50
56
  *,
51
57
  agent_name: str | None = None,
52
58
  agent_id: str | None = None,
59
+ ctx: TUIContext | None = None,
53
60
  ) -> tuple[int, int, int]:
54
61
  """Launch the Textual application and return the final pagination state.
55
62
 
@@ -59,6 +66,7 @@ def run_remote_runs_textual(
59
66
  callbacks: Data provider callback bundle.
60
67
  agent_name: Optional agent name for display purposes.
61
68
  agent_id: Optional agent ID for display purposes.
69
+ ctx: Shared TUI context.
62
70
 
63
71
  Returns:
64
72
  Tuple of (page, limit, cursor_index) after the UI exits.
@@ -69,15 +77,27 @@ def run_remote_runs_textual(
69
77
  callbacks,
70
78
  agent_name=agent_name,
71
79
  agent_id=agent_id,
80
+ ctx=ctx,
72
81
  )
73
82
  app.run()
74
83
  current_page = getattr(app, "current_page", initial_page)
75
84
  return current_page.page, current_page.limit, app.cursor_index
76
85
 
77
86
 
78
- class RunDetailScreen(ModalScreen[None]):
87
+ class RunDetailScreen(ToastHandlerMixin, ClipboardToastMixin, ModalScreen[None]):
79
88
  """Modal screen displaying run metadata and output timeline."""
80
89
 
90
+ CSS = """
91
+ Screen { layout: vertical; layers: base toasts; }
92
+ #toast-container {
93
+ width: 100%;
94
+ height: auto;
95
+ dock: top;
96
+ align: right top;
97
+ layer: toasts;
98
+ }
99
+ """
100
+
81
101
  BINDINGS = [
82
102
  Binding("escape", "dismiss", "Close", priority=True),
83
103
  Binding("q", "dismiss_modal", "Close", priority=True),
@@ -85,14 +105,24 @@ class RunDetailScreen(ModalScreen[None]):
85
105
  Binding("down", "scroll_down", "Down"),
86
106
  Binding("pageup", "page_up", "PgUp"),
87
107
  Binding("pagedown", "page_down", "PgDn"),
108
+ Binding("c", "copy_run_id", "Copy ID"),
109
+ Binding("C", "copy_detail_json", "Copy JSON"),
88
110
  Binding("e", "export_detail", "Export"),
89
111
  ]
90
112
 
91
- def __init__(self, detail: Any, on_export: Callable[[Any], None] | None = None):
113
+ def __init__(
114
+ self,
115
+ detail: Any,
116
+ on_export: Callable[[Any], None] | None = None,
117
+ ctx: TUIContext | None = None,
118
+ ) -> None:
92
119
  """Initialize the run detail screen."""
93
120
  super().__init__()
94
121
  self.detail = detail
95
122
  self._on_export = on_export
123
+ self._ctx = ctx
124
+ self._clipboard: ClipboardAdapter | None = None
125
+ self._local_toasts: ToastBus | None = None
96
126
 
97
127
  def compose(self) -> ComposeResult:
98
128
  """Render metadata and events."""
@@ -116,14 +146,17 @@ class RunDetailScreen(ModalScreen[None]):
116
146
  duration = self.detail.duration_formatted() if getattr(self.detail, "duration_formatted", None) else None
117
147
  add_meta("Duration", duration, "bold")
118
148
 
119
- yield Container(
149
+ main_content = Vertical(
120
150
  Static(meta_text, id="detail-meta"),
121
151
  RichLog(id="detail-events", wrap=False),
122
152
  )
153
+ yield main_content
154
+ yield ToastContainer(Toast(), id="toast-container")
123
155
  yield Footer()
124
156
 
125
157
  def on_mount(self) -> None:
126
158
  """Populate and focus the log."""
159
+ self._ensure_toast_bus()
127
160
  log = self.query_one("#detail-events", RichLog)
128
161
  log.can_focus = True
129
162
  log.write(Text("Events", style="bold"))
@@ -149,6 +182,61 @@ class RunDetailScreen(ModalScreen[None]):
149
182
  def _log(self) -> RichLog:
150
183
  return self.query_one("#detail-events", RichLog)
151
184
 
185
+ def action_copy_run_id(self) -> None:
186
+ """Copy the run id to the clipboard."""
187
+ run_id = getattr(self.detail, "id", None)
188
+ if not run_id:
189
+ self._announce_status("Run ID unavailable.")
190
+ return
191
+ self._copy_to_clipboard(str(run_id), label="Run ID")
192
+
193
+ def action_copy_detail_json(self) -> None:
194
+ """Copy the run detail JSON to the clipboard."""
195
+ payload = self._detail_json_payload()
196
+ if payload is None:
197
+ return
198
+ self._copy_to_clipboard(payload, label="Run JSON")
199
+
200
+ def _detail_json_payload(self) -> str | None:
201
+ detail = self.detail
202
+ if detail is None:
203
+ self._announce_status("Run detail unavailable.")
204
+ return None
205
+ if isinstance(detail, str):
206
+ return detail
207
+ if isinstance(detail, dict):
208
+ payload = detail
209
+ elif hasattr(detail, "model_dump"):
210
+ payload = detail.model_dump(mode="json")
211
+ elif hasattr(detail, "dict"):
212
+ payload = detail.dict()
213
+ else:
214
+ payload = getattr(detail, "__dict__", {"value": detail})
215
+ try:
216
+ return json.dumps(payload, indent=2, ensure_ascii=False, default=str)
217
+ except Exception as exc:
218
+ self._announce_status(f"Failed to serialize run detail: {exc}")
219
+ return None
220
+
221
+ def _append_copy_fallback(self, text: str) -> None:
222
+ try:
223
+ log = self._log()
224
+ except Exception:
225
+ self._announce_status(text)
226
+ return
227
+ log.write(Text(text))
228
+ log.write(Text(""))
229
+
230
+ def _ensure_toast_bus(self) -> None:
231
+ """Ensure toast bus is initialized and connected to message handler."""
232
+ if self._local_toasts is not None:
233
+ return # pragma: no cover - early return when already initialized
234
+
235
+ def _notify(m: ToastBus.Changed) -> None:
236
+ self.post_message(m)
237
+
238
+ self._local_toasts = ToastBus(on_change=_notify)
239
+
152
240
  @staticmethod
153
241
  def _status_style(status: str | None) -> str:
154
242
  """Return a Rich style name for the status pill."""
@@ -220,15 +308,25 @@ class RunDetailScreen(ModalScreen[None]):
220
308
  update_status(message, append=True)
221
309
 
222
310
 
223
- class RemoteRunsTextualApp(App[None]):
311
+ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
224
312
  """Textual application for browsing remote runs."""
225
313
 
226
314
  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; }}
315
+ #toast-container {{
316
+ width: 100%;
317
+ height: auto;
318
+ dock: top;
319
+ align: right top;
320
+ layer: toasts;
321
+ }}
322
+ #{RUNS_LOADING_ID} {{
323
+ width: auto;
324
+ display: none;
325
+ }}
326
+ #status-bar {{
327
+ height: 3;
328
+ padding: 0 1;
329
+ }}
232
330
  """
233
331
 
234
332
  BINDINGS = [
@@ -247,6 +345,7 @@ class RemoteRunsTextualApp(App[None]):
247
345
  *,
248
346
  agent_name: str | None = None,
249
347
  agent_id: str | None = None,
348
+ ctx: TUIContext | None = None,
250
349
  ):
251
350
  """Initialize the remote runs Textual application.
252
351
 
@@ -256,6 +355,7 @@ class RemoteRunsTextualApp(App[None]):
256
355
  callbacks: Callback bundle for data operations.
257
356
  agent_name: Optional agent name for display purposes.
258
357
  agent_id: Optional agent ID for display purposes.
358
+ ctx: Shared TUI context.
259
359
  """
260
360
  super().__init__()
261
361
  self.current_page = initial_page
@@ -265,6 +365,7 @@ class RemoteRunsTextualApp(App[None]):
265
365
  self.current_rows = initial_page.data[:]
266
366
  self.agent_name = (agent_name or "").strip()
267
367
  self.agent_id = (agent_id or "").strip()
368
+ self._ctx = ctx
268
369
  self._active_export_tasks: set[asyncio.Task[None]] = set()
269
370
  self._page_loader_task: asyncio.Task[Any] | None = None
270
371
  self._detail_loader_task: asyncio.Task[Any] | None = None
@@ -273,9 +374,10 @@ class RemoteRunsTextualApp(App[None]):
273
374
  def compose(self) -> ComposeResult:
274
375
  """Build layout."""
275
376
  yield Header()
276
- table = DataTable(id=RUNS_TABLE_ID)
277
- table.cursor_type = "row"
278
- table.add_columns(
377
+ yield ToastContainer(Toast(), id="toast-container")
378
+ table = DataTable(id=RUNS_TABLE_ID) # pragma: no cover - mocked in tests
379
+ table.cursor_type = "row" # pragma: no cover - mocked in tests
380
+ table.add_columns( # pragma: no cover - mocked in tests
279
381
  "Run UUID",
280
382
  "Type",
281
383
  "Status",
@@ -286,14 +388,24 @@ class RemoteRunsTextualApp(App[None]):
286
388
  )
287
389
  yield table # pragma: no cover - interactive UI, tested via integration
288
390
  yield Horizontal( # pragma: no cover - interactive UI, tested via integration
289
- LoadingIndicator(id=RUNS_LOADING_ID),
391
+ PulseIndicator(id=RUNS_LOADING_ID),
290
392
  Static(id="status"),
291
393
  id="status-bar",
292
394
  )
293
395
  yield Footer() # pragma: no cover - interactive UI, tested via integration
294
396
 
397
+ def _ensure_toast_bus(self) -> None:
398
+ if self._ctx is None or self._ctx.toasts is not None:
399
+ return
400
+
401
+ def _notify(m: ToastBus.Changed) -> None:
402
+ self.post_message(m)
403
+
404
+ self._ctx.toasts = ToastBus(on_change=_notify)
405
+
295
406
  def on_mount(self) -> None:
296
407
  """Render the initial page."""
408
+ self._ensure_toast_bus()
297
409
  self._hide_loading()
298
410
  self._render_page(self.current_page)
299
411
 
@@ -315,7 +427,7 @@ class RemoteRunsTextualApp(App[None]):
315
427
  if self.current_rows:
316
428
  self.cursor_index = max(0, min(self.cursor_index, len(self.current_rows) - 1))
317
429
  table.focus()
318
- table.cursor_coordinate = (self.cursor_index, 0)
430
+ table.cursor_coordinate = Coordinate(self.cursor_index, 0)
319
431
  self.current_page = runs_page
320
432
  total_pages = max(1, (runs_page.total + runs_page.limit - 1) // runs_page.limit)
321
433
  agent_display = self.agent_name or "Runs"
@@ -371,6 +483,26 @@ class RemoteRunsTextualApp(App[None]):
371
483
  """Track cursor position when DataTable selection changes."""
372
484
  self.cursor_index = getattr(event, "cursor_row", self.cursor_index)
373
485
 
486
+ def _handle_table_click(self, row: int | None) -> None:
487
+ if row is None:
488
+ return
489
+ table = self.query_one(RUNS_TABLE_SELECTOR, DataTable)
490
+ self.cursor_index = row
491
+ try:
492
+ table.cursor_coordinate = Coordinate(row, 0)
493
+ except Exception:
494
+ return
495
+ self.action_open_detail()
496
+
497
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: # pragma: no cover - UI hook
498
+ """Handle row selection event from DataTable."""
499
+ self._handle_table_click(getattr(event, "cursor_row", None))
500
+
501
+ def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: # pragma: no cover - UI hook
502
+ """Handle cell selection event from DataTable."""
503
+ row = getattr(event.coordinate, "row", None) if event.coordinate else None
504
+ self._handle_table_click(row)
505
+
374
506
  def action_page_left(self) -> None:
375
507
  """Navigate to the previous page."""
376
508
  if not self.current_page.has_prev:
@@ -413,7 +545,7 @@ class RemoteRunsTextualApp(App[None]):
413
545
  self._update_status("Already loading run detail. Please wait…", append=True)
414
546
  return
415
547
  run_id = str(run.id)
416
- self._show_loading("Loading run detail…", table_spinner=False)
548
+ self._show_loading("Loading run detail…", table_spinner=False, footer_message=False)
417
549
  self._queue_detail_load(run_id)
418
550
 
419
551
  async def action_export_run(self) -> None:
@@ -554,8 +686,8 @@ class RemoteRunsTextualApp(App[None]):
554
686
  if detail is None:
555
687
  self._update_status("Failed to load run detail.", append=True)
556
688
  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")
689
+ self.push_screen(RunDetailScreen(detail, on_export=self.queue_export_from_detail, ctx=self._ctx))
690
+ self._update_status("Detail view: ↑/↓ scroll · PgUp/PgDn · q/Esc close · c copy ID · C copy JSON · e export")
559
691
 
560
692
  def queue_export_from_detail(self, detail: Any) -> None:
561
693
  """Start an export from the detail modal."""
@@ -606,7 +738,7 @@ class RemoteRunsTextualApp(App[None]):
606
738
  show_loading_indicator(
607
739
  self,
608
740
  RUNS_LOADING_SELECTOR,
609
- message=message if footer_message else None,
741
+ message=message,
610
742
  set_status=self._update_status if footer_message else None,
611
743
  )
612
744
  self._set_table_loading(table_spinner)