zjcode 0.0.1__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 (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,252 @@
1
+ """Enhanced diff widget for displaying unified diffs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from textual.containers import Vertical
9
+ from textual.content import Content
10
+ from textual.widgets import Static
11
+
12
+ from deepagents_code import theme
13
+ from deepagents_code.config import get_glyphs, is_ascii_mode
14
+
15
+ if TYPE_CHECKING:
16
+ from textual.app import ComposeResult
17
+
18
+ _HUNK_RE = re.compile(r"@@ -(\d+)(?:,\d+)? \+(\d+)")
19
+ """Matches a unified-diff hunk header, capturing the old and new start lines."""
20
+
21
+
22
+ def compose_diff_lines(
23
+ diff: str,
24
+ max_lines: int | None = 100,
25
+ ) -> ComposeResult:
26
+ """Yield per-line Static widgets for a unified diff.
27
+
28
+ Each added/removed line gets a CSS class (`.diff-line-added`,
29
+ `.diff-line-removed`) so background colors are driven by CSS variables
30
+ and update automatically on theme change.
31
+
32
+ Args:
33
+ diff: Unified diff string.
34
+ max_lines: Maximum number of diff lines to show (None for unlimited).
35
+
36
+ Yields:
37
+ Static widgets — one per diff line — with appropriate CSS classes.
38
+ """
39
+ if not diff:
40
+ yield Static(Content.styled("No changes detected", "dim"))
41
+ else:
42
+ yield from _compose_diff_content(diff, max_lines)
43
+
44
+
45
+ def _compose_diff_content(
46
+ diff: str,
47
+ max_lines: int | None,
48
+ ) -> ComposeResult:
49
+ """Yield styled diff line widgets for non-empty diff content.
50
+
51
+ Args:
52
+ diff: Non-empty unified diff string.
53
+ max_lines: Maximum number of diff lines to show (None for unlimited).
54
+
55
+ Yields:
56
+ Static widgets for stats header and individual diff lines.
57
+ """
58
+ colors = theme.get_theme_colors()
59
+ glyphs = get_glyphs()
60
+ lines = diff.splitlines()
61
+
62
+ # Single pass for stats and max line number (width calculation).
63
+ additions = 0
64
+ deletions = 0
65
+ max_line = 0
66
+ for ln in lines:
67
+ if ln.startswith("+"):
68
+ if not ln.startswith("+++"):
69
+ additions += 1
70
+ elif ln.startswith("-"):
71
+ if not ln.startswith("---"):
72
+ deletions += 1
73
+ elif m := _HUNK_RE.match(ln):
74
+ max_line = max(max_line, int(m.group(1)), int(m.group(2)))
75
+
76
+ # Stats header
77
+ stats_parts: list[str | tuple[str, str] | Content] = []
78
+ if additions:
79
+ stats_parts.append((f"+{additions}", colors.success))
80
+ if deletions:
81
+ if stats_parts:
82
+ stats_parts.append(" ")
83
+ stats_parts.append((f"-{deletions}", colors.error))
84
+ if stats_parts:
85
+ yield Static(Content.assemble(*stats_parts))
86
+
87
+ width = max(3, len(str(max_line + len(lines))))
88
+
89
+ old_num = new_num = 0
90
+ line_count = 0
91
+
92
+ for line in lines:
93
+ if max_lines and line_count >= max_lines:
94
+ yield Static(
95
+ Content.styled(f"\n... ({len(lines) - line_count} more lines)", "dim")
96
+ )
97
+ break
98
+
99
+ # Skip file headers (--- and +++)
100
+ if line.startswith(("---", "+++")):
101
+ continue
102
+
103
+ # Handle hunk headers - just update line numbers, don't display
104
+ if m := _HUNK_RE.match(line):
105
+ old_num, new_num = int(m.group(1)), int(m.group(2))
106
+ continue
107
+
108
+ # Handle diff lines - use gutter bar instead of +/- prefix
109
+ content = line[1:] if line else ""
110
+
111
+ if line.startswith("-"):
112
+ # Deletion — red gutter bar, background via CSS
113
+ yield Static(
114
+ Content.assemble(
115
+ (f"{glyphs.gutter_bar}", f"{colors.error} bold"),
116
+ (f"{old_num:>{width}}", "dim"),
117
+ f" {content}",
118
+ ),
119
+ classes="diff-line-removed",
120
+ )
121
+ old_num += 1
122
+ line_count += 1
123
+ elif line.startswith("+"):
124
+ # Addition — green gutter bar, background via CSS
125
+ yield Static(
126
+ Content.assemble(
127
+ (f"{glyphs.gutter_bar}", f"{colors.success} bold"),
128
+ (f"{new_num:>{width}}", "dim"),
129
+ f" {content}",
130
+ ),
131
+ classes="diff-line-added",
132
+ )
133
+ new_num += 1
134
+ line_count += 1
135
+ elif line.startswith(" "):
136
+ # Context line — dim gutter
137
+ yield Static(
138
+ Content.assemble(
139
+ (f"{glyphs.box_vertical}{old_num:>{width}}", "dim"),
140
+ f" {content}",
141
+ ),
142
+ )
143
+ old_num += 1
144
+ new_num += 1
145
+ line_count += 1
146
+ elif line.strip() == "...":
147
+ # Truncation marker
148
+ yield Static(Content.styled("...", "dim"))
149
+ line_count += 1
150
+ else:
151
+ # Unrecognized diff line (e.g., "")
152
+ yield Static(Content.styled(line, "dim"))
153
+ line_count += 1
154
+
155
+
156
+ class EnhancedDiff(Vertical):
157
+ """Widget for displaying a unified diff with syntax highlighting."""
158
+
159
+ DEFAULT_CSS = """
160
+ EnhancedDiff {
161
+ height: auto;
162
+ padding: 1;
163
+ background: $surface-darken-1;
164
+ border: round $primary;
165
+ }
166
+
167
+ EnhancedDiff .diff-title {
168
+ color: $primary;
169
+ text-style: bold;
170
+ margin-bottom: 1;
171
+ }
172
+
173
+ EnhancedDiff .diff-content {
174
+ height: auto;
175
+ }
176
+
177
+ EnhancedDiff .diff-stats {
178
+ color: $text-muted;
179
+ margin-top: 1;
180
+ }
181
+ """
182
+
183
+ def __init__(
184
+ self,
185
+ diff: str,
186
+ title: str = "Diff",
187
+ max_lines: int | None = 100,
188
+ **kwargs: Any,
189
+ ) -> None:
190
+ """Initialize the diff widget.
191
+
192
+ Args:
193
+ diff: Unified diff string
194
+ title: Title to display above the diff
195
+ max_lines: Maximum number of diff lines to show
196
+ **kwargs: Additional arguments passed to parent
197
+ """
198
+ super().__init__(**kwargs)
199
+ self._diff = diff
200
+ self._title = title
201
+ self._max_lines = max_lines
202
+ self._stats = self._compute_stats()
203
+
204
+ def _compute_stats(self) -> tuple[int, int]:
205
+ """Compute additions and deletions count.
206
+
207
+ Returns:
208
+ Tuple of (additions count, deletions count).
209
+ """
210
+ additions = 0
211
+ deletions = 0
212
+ for line in self._diff.splitlines():
213
+ if line.startswith("+") and not line.startswith("+++"):
214
+ additions += 1
215
+ elif line.startswith("-") and not line.startswith("---"):
216
+ deletions += 1
217
+ return additions, deletions
218
+
219
+ def on_mount(self) -> None:
220
+ """Set border style based on charset mode."""
221
+ if is_ascii_mode():
222
+ colors = theme.get_theme_colors(self)
223
+ self.styles.border = ("ascii", colors.primary)
224
+
225
+ def compose(self) -> ComposeResult:
226
+ """Compose the diff widget layout.
227
+
228
+ Yields:
229
+ Widgets for title, formatted diff content, and stats.
230
+ """
231
+ colors = theme.get_theme_colors(self)
232
+ glyphs = get_glyphs()
233
+ h = glyphs.box_double_horizontal
234
+ yield Static(
235
+ Content.styled(
236
+ f"{h}{h}{h} {self._title} {h}{h}{h}", f"bold {colors.primary}"
237
+ ),
238
+ classes="diff-title",
239
+ )
240
+
241
+ yield from compose_diff_lines(self._diff, self._max_lines)
242
+
243
+ additions, deletions = self._stats
244
+ if additions or deletions:
245
+ content_parts: list[str | tuple[str, str]] = []
246
+ if additions:
247
+ content_parts.append((f"+{additions}", colors.success))
248
+ if deletions:
249
+ if content_parts:
250
+ content_parts.append(" ")
251
+ content_parts.append((f"-{deletions}", colors.error))
252
+ yield Static(Content.assemble(*content_parts), classes="diff-stats")
@@ -0,0 +1,189 @@
1
+ """Interactive reasoning effort selector for `/effort`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, ClassVar
6
+
7
+ from textual.binding import Binding, BindingType
8
+ from textual.containers import Vertical
9
+ from textual.content import Content
10
+ from textual.css.query import NoMatches
11
+ from textual.screen import ModalScreen
12
+ from textual.widgets import OptionList, Static
13
+ from textual.widgets.option_list import Option
14
+
15
+ if TYPE_CHECKING:
16
+ from textual.app import ComposeResult
17
+
18
+ from deepagents_code import theme
19
+ from deepagents_code.config import get_glyphs, is_ascii_mode
20
+
21
+
22
+ class EffortSelectorScreen(ModalScreen[str | None]):
23
+ """Modal dialog for selecting a reasoning effort level."""
24
+
25
+ BINDINGS: ClassVar[list[BindingType]] = [
26
+ Binding("escape", "cancel", "Cancel", show=False),
27
+ Binding("tab", "cursor_down", "Next", show=False, priority=True),
28
+ Binding("shift+tab", "cursor_up", "Previous", show=False, priority=True),
29
+ ]
30
+
31
+ CSS = """
32
+ EffortSelectorScreen {
33
+ align: center middle;
34
+ background: transparent;
35
+ }
36
+
37
+ EffortSelectorScreen > Vertical {
38
+ width: 54;
39
+ max-width: 90%;
40
+ height: auto;
41
+ max-height: 80%;
42
+ background: $surface;
43
+ border: solid $primary;
44
+ padding: 1 2;
45
+ }
46
+
47
+ EffortSelectorScreen .effort-selector-title {
48
+ text-style: bold;
49
+ color: $primary;
50
+ text-align: center;
51
+ margin-bottom: 1;
52
+ }
53
+
54
+ EffortSelectorScreen .effort-selector-subtitle {
55
+ height: auto;
56
+ color: $text-muted;
57
+ text-align: center;
58
+ margin-bottom: 1;
59
+ }
60
+
61
+ EffortSelectorScreen OptionList {
62
+ height: auto;
63
+ max-height: 10;
64
+ background: $background;
65
+ }
66
+
67
+ EffortSelectorScreen .effort-selector-help {
68
+ height: auto;
69
+ color: $text-muted;
70
+ text-style: italic;
71
+ margin-top: 1;
72
+ text-align: center;
73
+ }
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ model_spec: str,
80
+ efforts: tuple[str, ...],
81
+ current_effort: str | None = None,
82
+ default_effort: str | None = None,
83
+ ) -> None:
84
+ """Initialize the effort selector.
85
+
86
+ Args:
87
+ model_spec: Active `provider:model` spec.
88
+ efforts: Supported effort labels for `model_spec`.
89
+ current_effort: Current per-session effort override, if any.
90
+ default_effort: Provider default effort for `model_spec`, if known.
91
+ """
92
+ super().__init__()
93
+ self._model_spec = model_spec
94
+ self._efforts = efforts
95
+ self._current_effort = current_effort
96
+ self._default_effort = default_effort
97
+
98
+ def compose(self) -> ComposeResult:
99
+ """Compose the screen layout.
100
+
101
+ Yields:
102
+ Widgets for the effort selector UI.
103
+ """
104
+ glyphs = get_glyphs()
105
+ options = [
106
+ Option(self._format_label(effort), id=effort) for effort in self._efforts
107
+ ]
108
+ highlighted_effort = self._current_effort or self._default_effort
109
+ try:
110
+ highlighted = self._efforts.index(highlighted_effort)
111
+ except ValueError:
112
+ highlighted = 0
113
+ help_text = (
114
+ f"{glyphs.arrow_up}/{glyphs.arrow_down} or Tab switch"
115
+ f" {glyphs.bullet} Enter select"
116
+ f" {glyphs.bullet} Esc cancel"
117
+ )
118
+ with Vertical():
119
+ yield Static("Select Reasoning Effort", classes="effort-selector-title")
120
+ yield Static(self._model_spec, classes="effort-selector-subtitle")
121
+ option_list = OptionList(*options, id="effort-options")
122
+ option_list.highlighted = highlighted
123
+ yield option_list
124
+ yield Static(help_text, classes="effort-selector-help")
125
+
126
+ def _format_label(self, effort: str) -> Content:
127
+ """Render an effort label with a current marker.
128
+
129
+ Args:
130
+ effort: Effort label.
131
+
132
+ Returns:
133
+ Styled option label.
134
+ """
135
+ markers = []
136
+ if effort == self._current_effort:
137
+ markers.append("current")
138
+ if effort == self._default_effort:
139
+ markers.append("default")
140
+ if markers:
141
+ suffix = ", ".join(markers)
142
+ return Content.from_markup(
143
+ "$effort [dim]($suffix)[/dim]", effort=effort, suffix=suffix
144
+ )
145
+ return Content.from_markup("$effort", effort=effort)
146
+
147
+ def on_mount(self) -> None:
148
+ """Apply ASCII border if needed."""
149
+ if is_ascii_mode():
150
+ container = self.query_one(Vertical)
151
+ colors = theme.get_theme_colors(self)
152
+ container.styles.border = ("ascii", colors.success)
153
+
154
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
155
+ """Dismiss with the selected effort.
156
+
157
+ Args:
158
+ event: The option selected event.
159
+ """
160
+ effort = event.option.id
161
+ # Every option is built with a non-empty `id` (the effort label), so
162
+ # this is always truthy today. Guard anyway: a future id-less option
163
+ # (e.g. a separator) would otherwise dismiss with `None`, which reads
164
+ # as a cancel — a silent no-op rather than a clearly-impossible branch.
165
+ if effort is not None:
166
+ self.dismiss(effort)
167
+
168
+ def action_cancel(self) -> None:
169
+ """Cancel without changing effort."""
170
+ self.dismiss(None)
171
+
172
+ def action_cursor_down(self) -> None:
173
+ """Move the option list cursor down."""
174
+ option_list = self._option_list()
175
+ if option_list is not None:
176
+ option_list.action_cursor_down()
177
+
178
+ def action_cursor_up(self) -> None:
179
+ """Move the option list cursor up."""
180
+ option_list = self._option_list()
181
+ if option_list is not None:
182
+ option_list.action_cursor_up()
183
+
184
+ def _option_list(self) -> OptionList | None:
185
+ """Return the option list if it is mounted."""
186
+ try:
187
+ return self.query_one("#effort-options", OptionList)
188
+ except NoMatches:
189
+ return None