inspect-ai 0.3.49__py3-none-any.whl → 0.3.50__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 (93) hide show
  1. inspect_ai/_cli/info.py +2 -2
  2. inspect_ai/_cli/log.py +2 -2
  3. inspect_ai/_cli/score.py +2 -2
  4. inspect_ai/_display/core/display.py +19 -0
  5. inspect_ai/_display/core/panel.py +37 -7
  6. inspect_ai/_display/core/progress.py +29 -2
  7. inspect_ai/_display/core/results.py +79 -40
  8. inspect_ai/_display/core/textual.py +21 -0
  9. inspect_ai/_display/rich/display.py +28 -8
  10. inspect_ai/_display/textual/app.py +107 -1
  11. inspect_ai/_display/textual/display.py +1 -1
  12. inspect_ai/_display/textual/widgets/samples.py +132 -91
  13. inspect_ai/_display/textual/widgets/task_detail.py +232 -0
  14. inspect_ai/_display/textual/widgets/tasks.py +74 -6
  15. inspect_ai/_display/textual/widgets/toggle.py +32 -0
  16. inspect_ai/_eval/context.py +2 -0
  17. inspect_ai/_eval/eval.py +4 -3
  18. inspect_ai/_eval/loader.py +1 -1
  19. inspect_ai/_eval/run.py +35 -2
  20. inspect_ai/_eval/task/log.py +13 -11
  21. inspect_ai/_eval/task/results.py +12 -3
  22. inspect_ai/_eval/task/run.py +139 -36
  23. inspect_ai/_eval/task/sandbox.py +2 -1
  24. inspect_ai/_util/_async.py +30 -1
  25. inspect_ai/_util/file.py +31 -4
  26. inspect_ai/_util/html.py +3 -0
  27. inspect_ai/_util/logger.py +6 -5
  28. inspect_ai/_util/platform.py +5 -6
  29. inspect_ai/_util/registry.py +1 -1
  30. inspect_ai/_view/server.py +9 -9
  31. inspect_ai/_view/www/App.css +2 -2
  32. inspect_ai/_view/www/dist/assets/index.css +2 -2
  33. inspect_ai/_view/www/dist/assets/index.js +352 -294
  34. inspect_ai/_view/www/log-schema.json +13 -0
  35. inspect_ai/_view/www/package.json +1 -0
  36. inspect_ai/_view/www/src/components/MessageBand.mjs +1 -1
  37. inspect_ai/_view/www/src/components/Tools.mjs +16 -13
  38. inspect_ai/_view/www/src/samples/SampleDisplay.mjs +1 -3
  39. inspect_ai/_view/www/src/samples/SampleScoreView.mjs +52 -77
  40. inspect_ai/_view/www/src/samples/SamplesDescriptor.mjs +38 -13
  41. inspect_ai/_view/www/src/samples/transcript/ModelEventView.mjs +15 -2
  42. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.mjs +4 -2
  43. inspect_ai/_view/www/src/types/log.d.ts +2 -0
  44. inspect_ai/_view/www/src/workspace/WorkSpace.mjs +2 -0
  45. inspect_ai/_view/www/yarn.lock +9 -4
  46. inspect_ai/approval/__init__.py +1 -1
  47. inspect_ai/approval/_human/approver.py +35 -0
  48. inspect_ai/approval/_human/console.py +62 -0
  49. inspect_ai/approval/_human/manager.py +108 -0
  50. inspect_ai/approval/_human/panel.py +233 -0
  51. inspect_ai/approval/_human/util.py +51 -0
  52. inspect_ai/dataset/_sources/hf.py +2 -2
  53. inspect_ai/dataset/_sources/util.py +1 -1
  54. inspect_ai/log/_file.py +106 -36
  55. inspect_ai/log/_recorders/eval.py +226 -158
  56. inspect_ai/log/_recorders/file.py +9 -6
  57. inspect_ai/log/_recorders/json.py +35 -12
  58. inspect_ai/log/_recorders/recorder.py +15 -15
  59. inspect_ai/log/_samples.py +52 -0
  60. inspect_ai/model/_model.py +14 -0
  61. inspect_ai/model/_model_output.py +4 -0
  62. inspect_ai/model/_providers/azureai.py +1 -1
  63. inspect_ai/model/_providers/hf.py +106 -4
  64. inspect_ai/model/_providers/util/__init__.py +2 -0
  65. inspect_ai/model/_providers/util/hf_handler.py +200 -0
  66. inspect_ai/scorer/_common.py +1 -1
  67. inspect_ai/solver/_plan.py +0 -8
  68. inspect_ai/solver/_task_state.py +18 -1
  69. inspect_ai/solver/_use_tools.py +9 -1
  70. inspect_ai/tool/_tool_def.py +2 -2
  71. inspect_ai/tool/_tool_info.py +14 -2
  72. inspect_ai/tool/_tool_params.py +2 -1
  73. inspect_ai/tool/_tools/_execute.py +1 -1
  74. inspect_ai/tool/_tools/_web_browser/_web_browser.py +6 -0
  75. inspect_ai/util/__init__.py +5 -6
  76. inspect_ai/util/_panel.py +91 -0
  77. inspect_ai/util/_sandbox/__init__.py +2 -6
  78. inspect_ai/util/_sandbox/context.py +4 -3
  79. inspect_ai/util/_sandbox/docker/compose.py +12 -2
  80. inspect_ai/util/_sandbox/docker/docker.py +19 -9
  81. inspect_ai/util/_sandbox/docker/util.py +10 -2
  82. inspect_ai/util/_sandbox/environment.py +47 -41
  83. inspect_ai/util/_sandbox/local.py +15 -10
  84. inspect_ai/util/_subprocess.py +43 -3
  85. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/METADATA +2 -2
  86. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/RECORD +90 -82
  87. inspect_ai/_view/www/node_modules/flatted/python/flatted.py +0 -149
  88. inspect_ai/_view/www/node_modules/flatted/python/test.py +0 -63
  89. inspect_ai/approval/_human.py +0 -123
  90. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/LICENSE +0 -0
  91. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/WHEEL +0 -0
  92. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/entry_points.txt +0 -0
  93. {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,29 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  from asyncio import CancelledError
4
- from typing import Any, AsyncIterator, Coroutine, Generic, Iterator
4
+ from typing import Any, AsyncIterator, ClassVar, Coroutine, Generic, Iterator, cast
5
5
 
6
6
  import rich
7
7
  from rich.console import Console
8
8
  from rich.text import Text
9
9
  from textual.app import App, ComposeResult
10
+ from textual.binding import Binding, BindingType
11
+ from textual.css.query import NoMatches
10
12
  from textual.events import Print
11
13
  from textual.widgets import TabbedContent, TabPane
14
+ from textual.widgets.tabbed_content import ContentTabs
12
15
  from textual.worker import Worker, WorkerState
13
16
  from typing_extensions import override
14
17
 
18
+ from inspect_ai._display.core.textual import textual_enable_mouse_support
19
+ from inspect_ai._util.html import as_html_id
15
20
  from inspect_ai.log._samples import active_samples
16
21
  from inspect_ai.log._transcript import InputEvent, transcript
17
22
 
23
+ from ...util._panel import InputPanel
18
24
  from ..core.config import task_config
19
25
  from ..core.display import (
26
+ TP,
20
27
  TR,
21
28
  TaskDisplay,
22
29
  TaskProfile,
@@ -50,6 +57,17 @@ class TaskScreenResult(Generic[TR]):
50
57
  class TaskScreenApp(App[TR]):
51
58
  CSS_PATH = "app.tcss"
52
59
 
60
+ BINDINGS: ClassVar[list[BindingType]] = [
61
+ Binding(
62
+ "ctrl+c",
63
+ "quit",
64
+ "Interrupt",
65
+ tooltip="Interrupt the app and return to the command prompt.",
66
+ show=False,
67
+ priority=True,
68
+ )
69
+ ]
70
+
53
71
  def __init__(self) -> None:
54
72
  # call super
55
73
  super().__init__()
@@ -70,6 +88,12 @@ class TaskScreenApp(App[TR]):
70
88
  # enable rich hooks
71
89
  rich_initialise()
72
90
 
91
+ def _watch_app_focus(self, focus: bool) -> None:
92
+ super()._watch_app_focus(focus)
93
+
94
+ if focus and self.app._driver:
95
+ textual_enable_mouse_support(self.app._driver)
96
+
73
97
  def run_app(self, main: Coroutine[Any, Any, TR]) -> TaskScreenResult[TR]:
74
98
  # create the worker
75
99
  self._worker = self.run_worker(main, start=False, exit_on_error=False)
@@ -217,6 +241,8 @@ class TaskScreenApp(App[TR]):
217
241
  self.update_tasks()
218
242
  self.update_samples()
219
243
  self.update_footer()
244
+ for input_panel in self.query(f".{InputPanel.DEFAULT_CLASSES}"):
245
+ cast(InputPanel, input_panel).update()
220
246
 
221
247
  # update the header title
222
248
  def update_title(self) -> None:
@@ -288,6 +314,12 @@ class TaskScreenApp(App[TR]):
288
314
 
289
315
  self.watch(tabs, "active", set_active_tab)
290
316
 
317
+ # activate the tasks tab
318
+ def activate_tasks_tab(self) -> None:
319
+ tasks = self.query_one(TasksView)
320
+ tasks.tasks.focus() # force the tab to switch by focusing a child
321
+ self.query_one(ContentTabs).focus() # focus the tab control
322
+
291
323
  # capture output and route to console view and our buffer
292
324
  def on_print(self, event: Print) -> None:
293
325
  # remove trailing newline
@@ -308,10 +340,66 @@ class TaskScreenApp(App[TR]):
308
340
  self._worker.cancel()
309
341
  self.update_title()
310
342
 
343
+ # dynamic input panels
344
+ async def add_input_panel(self, title: str, panel: InputPanel) -> None:
345
+ tabs = self.query_one(TabbedContent)
346
+ await tabs.add_pane(TabPane(title, panel, id=as_input_panel_id(title)))
347
+
348
+ def get_input_panel(self, title: str) -> InputPanel | None:
349
+ try:
350
+ tab_pane = self.query_one(f"#{as_input_panel_id(title)}")
351
+ if len(tab_pane.children) > 0:
352
+ return cast(InputPanel, tab_pane.children[0])
353
+ else:
354
+ return None
355
+ except NoMatches:
356
+ return None
357
+
358
+ async def remove_input_panel(self, title: str) -> None:
359
+ tabs = self.query_one(TabbedContent)
360
+ await tabs.remove_pane(as_html_id(as_input_panel_id(title), title))
361
+
362
+ class InputPanelHost(InputPanel.Host):
363
+ def __init__(self, app: "TaskScreenApp[TR]", tab_id: str) -> None:
364
+ self.app = app
365
+ self.tab_id = tab_id
366
+
367
+ def set_title(self, title: str) -> None:
368
+ tabs = self.app.query_one(TabbedContent)
369
+ tab = tabs.get_tab(self.tab_id)
370
+ tab.label = Text.from_markup(title)
371
+
372
+ def activate(self) -> None:
373
+ # show the tab
374
+ tabs = self.app.query_one(TabbedContent)
375
+ tabs.show_tab(self.tab_id)
376
+
377
+ # focus the first focuable child (this seems to be necessary
378
+ # to get textual to reliably make the switch). after that, focus
379
+ # the tabs control so the user can switch back w/ the keyboard
380
+ tab_pane = self.app.query_one(f"#{self.tab_id}")
381
+ panel = cast(InputPanel, tab_pane.children[0])
382
+ for child in panel.children:
383
+ if child.focusable:
384
+ child.focus()
385
+ self.app.query_one(ContentTabs).focus()
386
+ break
387
+
388
+ def deactivate(self) -> None:
389
+ tabs = self.app.query_one(TabbedContent)
390
+ if tabs.active == self.tab_id:
391
+ self.app.activate_tasks_tab()
392
+
393
+ def close(self) -> None:
394
+ tabs = self.app.query_one(TabbedContent)
395
+ tabs.remove_pane(self.tab_id)
396
+ self.app.activate_tasks_tab()
397
+
311
398
 
312
399
  class TextualTaskScreen(TaskScreen, Generic[TR]):
313
400
  def __init__(self, app: TaskScreenApp[TR]) -> None:
314
401
  self.app = app
402
+ self.lock = asyncio.Lock()
315
403
 
316
404
  def __exit__(self, *excinfo: Any) -> None:
317
405
  pass
@@ -361,3 +449,21 @@ class TextualTaskScreen(TaskScreen, Generic[TR]):
361
449
  # reset width
362
450
  if old_width:
363
451
  console.width = old_width
452
+
453
+ @override
454
+ async def input_panel(self, title: str, panel: type[TP]) -> TP:
455
+ async with self.lock:
456
+ panel_widget = self.app.get_input_panel(title)
457
+ if panel_widget is None:
458
+ panel_widget = panel(
459
+ title,
460
+ TaskScreenApp[TR].InputPanelHost(
461
+ self.app, as_input_panel_id(title)
462
+ ),
463
+ )
464
+ await self.app.add_input_panel(title, panel_widget)
465
+ return cast(TP, panel_widget)
466
+
467
+
468
+ def as_input_panel_id(title: str) -> str:
469
+ return as_html_id("id-input-panel", title)
@@ -53,7 +53,7 @@ class TextualDisplay(Display):
53
53
  @override
54
54
  @contextlib.contextmanager
55
55
  def suspend_task_app(self) -> Iterator[None]:
56
- if getattr(self, "app", None):
56
+ if getattr(self, "app", None) and self.app.is_running:
57
57
  with self.app.suspend_app():
58
58
  yield
59
59
  else:
@@ -1,5 +1,7 @@
1
+ import time
1
2
  from typing import cast
2
3
 
4
+ from rich.console import RenderableType
3
5
  from rich.table import Table
4
6
  from rich.text import Text
5
7
  from textual.app import ComposeResult
@@ -9,6 +11,7 @@ from textual.containers import (
9
11
  Vertical,
10
12
  VerticalGroup,
11
13
  )
14
+ from textual.reactive import reactive
12
15
  from textual.widget import Widget
13
16
  from textual.widgets import (
14
17
  Button,
@@ -21,11 +24,6 @@ from textual.widgets.option_list import Option, Separator
21
24
 
22
25
  from inspect_ai._util.registry import registry_unqualified_name
23
26
  from inspect_ai.log._samples import ActiveSample
24
- from inspect_ai.util._sandbox import (
25
- SandboxConnection,
26
- SandboxConnectionContainer,
27
- SandboxConnectionLocal,
28
- )
29
27
 
30
28
  from ...core.progress import progress_time
31
29
  from .clock import Clock
@@ -49,6 +47,7 @@ class SamplesView(Widget):
49
47
  def __init__(self) -> None:
50
48
  super().__init__()
51
49
  self.samples: list[ActiveSample] = []
50
+ self.last_updated = time.perf_counter()
52
51
 
53
52
  def compose(self) -> ComposeResult:
54
53
  yield SamplesList()
@@ -65,7 +64,12 @@ class SamplesView(Widget):
65
64
  await self.query_one(TranscriptView).notify_active(active)
66
65
 
67
66
  def set_samples(self, samples: list[ActiveSample]) -> None:
68
- self.query_one(SamplesList).set_samples(samples)
67
+ # throttle to no more than 1 second per 100 samples
68
+ throttle = round(max(len(samples) / 100, 1))
69
+ current = time.perf_counter()
70
+ if (current - self.last_updated) > throttle:
71
+ self.query_one(SamplesList).set_samples(samples)
72
+ self.last_updated = current
69
73
 
70
74
  async def set_highlighted_sample(self, highlighted: int | None) -> None:
71
75
  sample_info = self.query_one(SampleInfo)
@@ -198,7 +202,7 @@ class SampleInfo(Horizontal):
198
202
  }
199
203
  SampleInfo Collapsible Contents {
200
204
  padding: 1 0 1 2;
201
- overflow-y: hidden;
205
+ height: auto;
202
206
  overflow-x: auto;
203
207
  }
204
208
  SampleInfo Static {
@@ -211,52 +215,89 @@ class SampleInfo(Horizontal):
211
215
  def __init__(self) -> None:
212
216
  super().__init__()
213
217
  self._sample: ActiveSample | None = None
214
- self._show_sandboxes = False
215
218
 
216
219
  def compose(self) -> ComposeResult:
217
- if self._sample is not None and len(self._sample.sandboxes) > 0:
218
- with Collapsible(title=""):
219
- yield SandboxesView()
220
- else:
221
- yield Static()
220
+ with Collapsible(title=""):
221
+ yield SampleLimits()
222
+ yield SandboxesView()
222
223
 
223
224
  async def sync_sample(self, sample: ActiveSample | None) -> None:
224
- # bail if we've already processed this sample
225
- if self._sample == sample:
226
- return
225
+ if sample is None:
226
+ self.display = False
227
+ self._sample = None
228
+ else:
229
+ # update sample limits
230
+ limits = self.query_one(SampleLimits)
231
+ await limits.sync_sample(sample)
227
232
 
228
- # set sample
229
- self._sample = sample
233
+ # bail if we've already processed this sample
234
+ if self._sample == sample:
235
+ return
230
236
 
231
- # compute whether we should show connection and recompose as required
232
- show_sandboxes = sample is not None and len(sample.sandboxes) > 0
233
- if show_sandboxes != self._show_sandboxes:
234
- await self.recompose()
235
- self._show_sandboxes = show_sandboxes
237
+ # set sample
238
+ self._sample = sample
236
239
 
237
- if sample is not None:
240
+ # update UI
238
241
  self.display = True
239
242
  title = f"{registry_unqualified_name(sample.task)} (id: {sample.sample.id}, epoch {sample.epoch}): {sample.model}"
240
- if show_sandboxes:
241
- self.query_one(Collapsible).title = title
242
- sandboxes = self.query_one(SandboxesView)
243
- await sandboxes.sync_sandboxes(sample.sandboxes)
244
- else:
245
- self.query_one(Static).update(title)
246
- else:
247
- self.display = False
243
+ self.query_one(Collapsible).title = title
244
+ sandboxes = self.query_one(SandboxesView)
245
+ await sandboxes.sync_sample(sample)
246
+
247
+
248
+ class SampleLimits(Widget):
249
+ DEFAULT_CSS = """
250
+ SampleLimits {
251
+ padding: 0 0 0 0;
252
+ color: $secondary;
253
+ background: transparent;
254
+ height: auto;
255
+ }
256
+ SampleLimits Static {
257
+ background: transparent;
258
+ color: $secondary;
259
+ }
260
+ """
261
+
262
+ messages = reactive(0)
263
+ message_limit = reactive(0)
264
+ tokens = reactive(0)
265
+ token_limit = reactive(0)
266
+ started = reactive(0)
267
+ time_limit = reactive(0)
268
+
269
+ def __init__(self) -> None:
270
+ super().__init__()
271
+
272
+ def render(self) -> RenderableType:
273
+ limits = f"[bold]messages[/bold]: {self.messages}"
274
+ if self.message_limit:
275
+ limits = f"{limits} (limit {self.message_limit})"
276
+ limits = f"{limits}, [bold]tokens[/bold]: {self.tokens:,}"
277
+ if self.token_limit:
278
+ limits = f"{limits} ({self.token_limit:,})"
279
+ return limits
280
+
281
+ async def sync_sample(self, sample: ActiveSample) -> None:
282
+ self.messages = sample.total_messages
283
+ self.message_limit = sample.message_limit or 0
284
+ self.tokens = sample.total_tokens
285
+ self.token_limit = sample.token_limit or 0
248
286
 
249
287
 
250
288
  class SandboxesView(Vertical):
251
289
  DEFAULT_CSS = """
252
290
  SandboxesView {
253
- padding: 0 0 1 0;
291
+ padding: 1 0 1 0;
254
292
  background: transparent;
255
293
  height: auto;
256
294
  }
257
295
  SandboxesView Static {
258
296
  background: transparent;
259
297
  }
298
+ .clipboard-message {
299
+ margin-top: 1;
300
+ }
260
301
  """
261
302
 
262
303
  def __init__(self) -> None:
@@ -264,61 +305,57 @@ class SandboxesView(Vertical):
264
305
 
265
306
  def compose(self) -> ComposeResult:
266
307
  yield Static(id="sandboxes-caption", markup=True)
267
- yield Vertical(id="sandboxes")
268
- yield Static(
269
- "[italic]Hold down Alt (or Option) to select text for copying[/italic]",
270
- id="sandboxes-footer",
271
- markup=True,
272
- )
273
-
274
- async def sync_sandboxes(self, sandboxes: dict[str, SandboxConnection]) -> None:
275
- def sandbox_connection_type() -> str:
276
- connection = list(sandboxes.values())[0]
277
- if isinstance(connection, SandboxConnectionLocal):
278
- return "directories"
279
- elif isinstance(connection, SandboxConnectionContainer):
280
- return "containers"
281
- else:
282
- return "hosts"
308
+ yield Vertical(id="sandboxes-list")
283
309
 
284
- def sandbox_connection_target(sandbox: SandboxConnection) -> str:
285
- if isinstance(sandbox, SandboxConnectionLocal):
286
- target = sandbox.working_dir
287
- elif isinstance(sandbox, SandboxConnectionContainer):
288
- target = sandbox.container
289
- else:
290
- target = sandbox.destination
291
- return target.strip()
292
-
293
- caption = cast(Static, self.query_one("#sandboxes-caption"))
294
- caption.update(f"[bold]sandbox {sandbox_connection_type()}:[/bold]")
295
-
296
- sandboxes_widget = self.query_one("#sandboxes")
297
- sandboxes_widget.styles.margin = (
298
- (0, 0, 1, 0) if len(sandboxes) > 1 else (0, 0, 0, 0)
299
- )
300
- await sandboxes_widget.remove_children()
301
- await sandboxes_widget.mount_all(
302
- [
303
- Static(sandbox_connection_target(sandbox))
304
- for sandbox in sandboxes.values()
305
- ]
310
+ async def sync_sample(self, sample: ActiveSample) -> None:
311
+ sandboxes = sample.sandboxes
312
+ show_sandboxes = (
313
+ len([sandbox for sandbox in sandboxes.values() if sandbox.container]) > 0
306
314
  )
307
315
 
316
+ if show_sandboxes:
317
+ self.display = True
318
+ sandboxes_caption = cast(Static, self.query_one("#sandboxes-caption"))
319
+ sandboxes_caption.update("[bold]sandbox containers:[/bold]")
320
+
321
+ sandboxes_list = self.query_one("#sandboxes-list")
322
+ await sandboxes_list.remove_children()
323
+ await sandboxes_list.mount_all(
324
+ [
325
+ Static(sandbox.container)
326
+ for sandbox in sandboxes.values()
327
+ if sandbox.container
328
+ ]
329
+ )
330
+ sandboxes_list.mount(
331
+ Static(
332
+ "[italic]Hold down Alt (or Option) to select text for copying[/italic]",
333
+ classes="clipboard-message",
334
+ markup=True,
335
+ )
336
+ )
337
+ else:
338
+ self.display = False
339
+
308
340
 
309
341
  class SampleToolbar(Horizontal):
310
- DEFAULT_CSS = """
311
- SampleToolbar Button {
342
+ CANCEL_SCORE_OUTPUT = "cancel_score_output"
343
+ CANCEL_RAISE_ERROR = "cancel_raise_error"
344
+ PENDING_STATUS = "pending_status"
345
+ PENDING_CAPTION = "pending_caption"
346
+
347
+ DEFAULT_CSS = f"""
348
+ SampleToolbar Button {{
312
349
  margin-bottom: 1;
313
350
  margin-right: 2;
314
351
  min-width: 20;
315
- }
316
- SampleToolbar #cancel-score-output {
352
+ }}
353
+ SampleToolbar #{CANCEL_SCORE_OUTPUT} {{
317
354
  color: $primary-darken-3;
318
- }
319
- SampleToolbar #cancel-raise-error {
355
+ }}
356
+ SampleToolbar #{CANCEL_RAISE_ERROR} {{
320
357
  color: $warning-darken-3;
321
- }
358
+ }}
322
359
  """
323
360
 
324
361
  def __init__(self) -> None:
@@ -326,30 +363,30 @@ class SampleToolbar(Horizontal):
326
363
  self.sample: ActiveSample | None = None
327
364
 
328
365
  def compose(self) -> ComposeResult:
329
- with VerticalGroup(id="pending-status"):
330
- yield Static("Executing...", id="pending-caption")
366
+ with VerticalGroup(id=self.PENDING_STATUS):
367
+ yield Static("Executing...", id=self.PENDING_CAPTION)
331
368
  yield HorizontalGroup(EventLoadingIndicator(), Clock())
332
369
  yield Button(
333
370
  Text("Cancel (Score)"),
334
- id="cancel-score-output",
371
+ id=self.CANCEL_SCORE_OUTPUT,
335
372
  tooltip="Cancel the sample and score whatever output has been generated so far.",
336
373
  )
337
374
  yield Button(
338
375
  Text("Cancel (Error)"),
339
- id="cancel-raise-error",
376
+ id=self.CANCEL_RAISE_ERROR,
340
377
  tooltip="Cancel the sample and raise an error (task will exit unless fail_on_error is set)",
341
378
  )
342
379
 
343
380
  def on_mount(self) -> None:
344
- self.query_one("#pending-status").visible = False
345
- self.query_one("#cancel-score-output").display = False
346
- self.query_one("#cancel-raise-error").display = False
381
+ self.query_one("#" + self.PENDING_STATUS).visible = False
382
+ self.query_one("#" + self.CANCEL_SCORE_OUTPUT).display = False
383
+ self.query_one("#" + self.CANCEL_RAISE_ERROR).display = False
347
384
 
348
385
  def on_button_pressed(self, event: Button.Pressed) -> None:
349
386
  if self.sample:
350
- if event.button.id == "cancel-score-output":
387
+ if event.button.id == self.CANCEL_SCORE_OUTPUT:
351
388
  self.sample.interrupt("score")
352
- elif event.button.id == "cancel-raise-error":
389
+ elif event.button.id == self.CANCEL_RAISE_ERROR:
353
390
  self.sample.interrupt("error")
354
391
 
355
392
  async def sync_sample(self, sample: ActiveSample | None) -> None:
@@ -358,10 +395,12 @@ class SampleToolbar(Horizontal):
358
395
  # track the sample
359
396
  self.sample = sample
360
397
 
361
- pending_status = self.query_one("#pending-status")
398
+ pending_status = self.query_one("#" + self.PENDING_STATUS)
362
399
  clock = self.query_one(Clock)
363
- cancel_score_output = cast(Button, self.query_one("#cancel-score-output"))
364
- cancel_with_error = cast(Button, self.query_one("#cancel-raise-error"))
400
+ cancel_score_output = cast(
401
+ Button, self.query_one("#" + self.CANCEL_SCORE_OUTPUT)
402
+ )
403
+ cancel_with_error = cast(Button, self.query_one("#" + self.CANCEL_RAISE_ERROR))
365
404
  if sample and not sample.completed:
366
405
  # update visibility and button status
367
406
  self.display = True
@@ -376,7 +415,9 @@ class SampleToolbar(Horizontal):
376
415
  )
377
416
  if last_event and last_event.pending:
378
417
  pending_status.visible = True
379
- pending_caption = cast(Static, self.query_one("#pending-caption"))
418
+ pending_caption = cast(
419
+ Static, self.query_one("#" + self.PENDING_CAPTION)
420
+ )
380
421
  pending_caption_text = (
381
422
  "Generating..."
382
423
  if isinstance(last_event, ModelEvent)