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,401 @@
1
+ """Interactive theme selector screen for `/theme` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
9
+
10
+ from textual.binding import Binding, BindingType
11
+ from textual.containers import Vertical
12
+ from textual.screen import ModalScreen
13
+ from textual.widgets import OptionList, Static
14
+ from textual.widgets.option_list import Option
15
+
16
+ if TYPE_CHECKING:
17
+ from textual.app import ComposeResult
18
+
19
+ from deepagents_code import theme
20
+ from deepagents_code.config import get_glyphs, is_ascii_mode
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @runtime_checkable
26
+ class _TerminalBackgroundSyncApp(Protocol):
27
+ """App protocol for terminal background sync after theme preview changes."""
28
+
29
+ def sync_terminal_background(self) -> None:
30
+ """Sync the terminal background to the active theme."""
31
+
32
+
33
+ class ThemeSelectorScreen(ModalScreen[str | None]):
34
+ """Modal dialog for theme selection with live preview.
35
+
36
+ Displays available themes in an `OptionList`. Navigating the option list
37
+ applies a live preview by swapping the app theme. Returns the selected
38
+ theme name on Enter, or `None` on Esc. Esc normally restores the original
39
+ theme, but if a per-terminal default was saved with `t` this session, Esc
40
+ keeps that theme active instead of reverting.
41
+ """
42
+
43
+ BINDINGS: ClassVar[list[BindingType]] = [
44
+ Binding("escape", "cancel", "Cancel", show=False),
45
+ Binding("tab", "cursor_down", "Next", show=False, priority=True),
46
+ Binding("shift+tab", "cursor_up", "Previous", show=False, priority=True),
47
+ Binding("n", "toggle_names", "Names", show=False),
48
+ Binding("t", "set_for_terminal", "Set for terminal", show=False),
49
+ ]
50
+ """Key bindings for the selector.
51
+
52
+ Esc dismisses, restoring the original theme unless a `t` save set a
53
+ per-terminal default this session (in which case that theme is kept).
54
+ Arrow keys and Enter are handled natively by the embedded `OptionList`;
55
+ Tab / Shift+Tab are bound
56
+ here to advance the option list cursor for consistency with other
57
+ selector screens (where Tab cycles focus across multiple widgets).
58
+ `action_toggle_names` toggles between human-readable labels and canonical
59
+ registry keys, which are accepted by the theme config. The terminal-default
60
+ action saves the highlighted theme for the current terminal and updates
61
+ the `(default)` badge in place without closing the picker.
62
+ """
63
+
64
+ CSS = """
65
+ ThemeSelectorScreen {
66
+ align: center middle;
67
+ background: transparent;
68
+ }
69
+
70
+ ThemeSelectorScreen > Vertical {
71
+ width: 50;
72
+ max-width: 90%;
73
+ height: auto;
74
+ max-height: 80%;
75
+ background: $surface;
76
+ border: solid $primary;
77
+ padding: 1 2;
78
+ }
79
+
80
+ ThemeSelectorScreen .theme-selector-title {
81
+ text-style: bold;
82
+ color: $primary;
83
+ text-align: center;
84
+ margin-bottom: 1;
85
+ }
86
+
87
+ ThemeSelectorScreen OptionList {
88
+ height: auto;
89
+ max-height: 16;
90
+ background: $background;
91
+ }
92
+
93
+ ThemeSelectorScreen .theme-selector-help {
94
+ height: auto;
95
+ color: $text-muted;
96
+ text-style: italic;
97
+ margin-top: 1;
98
+ text-align: center;
99
+ }
100
+ """
101
+ """Styling for the centered modal shell, title, option list, and help footer."""
102
+
103
+ def __init__(self, current_theme: str, terminal_default: str | None = None) -> None:
104
+ """Initialize the ThemeSelectorScreen.
105
+
106
+ Args:
107
+ current_theme: The currently active theme name (to highlight).
108
+ terminal_default: The theme saved in `[ui.terminal_themes]` for
109
+ the current `TERM_PROGRAM`, if any. Badged with `(default)`
110
+ in the option list.
111
+ """
112
+ super().__init__()
113
+ self._current_theme = current_theme
114
+ self._original_theme = current_theme
115
+ self._terminal_default = terminal_default
116
+ self._session_terminal_default: str | None = None
117
+ self._cancel_kept_terminal_default: str | None = None
118
+ self._show_keys = False
119
+
120
+ def _sync_terminal_background(self) -> None:
121
+ """Ask the app to sync terminal background after preview changes."""
122
+ if isinstance(self.app, _TerminalBackgroundSyncApp):
123
+ self.app.sync_terminal_background()
124
+
125
+ def _format_option(self, name: str, entry: theme.ThemeEntry) -> str:
126
+ """Render the option text for a theme entry.
127
+
128
+ Args:
129
+ name: Registry key.
130
+ entry: Registry entry.
131
+
132
+ Returns:
133
+ Either the human label or the registry key, with `(current)`
134
+ and/or `(default)` suffixes — combined as
135
+ `(current, default)` when both apply to the same theme.
136
+ """
137
+ text = name if self._show_keys else entry.label
138
+ suffixes: list[str] = []
139
+ if name == self._current_theme:
140
+ suffixes.append("current")
141
+ if name == self._terminal_default:
142
+ suffixes.append("default")
143
+ if suffixes:
144
+ text = f"{text} ({', '.join(suffixes)})"
145
+ return text
146
+
147
+ def compose(self) -> ComposeResult:
148
+ """Compose the screen layout.
149
+
150
+ Yields:
151
+ Widgets for the theme selector UI.
152
+ """
153
+ glyphs = get_glyphs()
154
+ options: list[Option] = []
155
+ highlight_index = 0
156
+
157
+ for i, (name, entry) in enumerate(theme.get_registry().items()):
158
+ options.append(Option(self._format_option(name, entry), id=name))
159
+ if name == self._current_theme:
160
+ highlight_index = i
161
+
162
+ with Vertical():
163
+ yield Static("Select Theme", classes="theme-selector-title")
164
+ option_list = OptionList(*options, id="theme-options")
165
+ option_list.highlighted = highlight_index
166
+ yield option_list
167
+ nav_line = (
168
+ f"{glyphs.arrow_up}/{glyphs.arrow_down} or Tab switch"
169
+ f" {glyphs.bullet} Enter select"
170
+ f" {glyphs.bullet} Esc cancel"
171
+ )
172
+ action_line = f"N labels/keys {glyphs.bullet} T set for this terminal"
173
+ yield Static(f"{nav_line}\n{action_line}", classes="theme-selector-help")
174
+
175
+ def on_mount(self) -> None:
176
+ """Apply ASCII border if needed."""
177
+ if is_ascii_mode():
178
+ container = self.query_one(Vertical)
179
+ colors = theme.get_theme_colors(self)
180
+ container.styles.border = ("ascii", colors.success)
181
+
182
+ def on_option_list_option_highlighted(
183
+ self, event: OptionList.OptionHighlighted
184
+ ) -> None:
185
+ """Live-preview the highlighted theme.
186
+
187
+ Args:
188
+ event: The option highlighted event.
189
+ """
190
+ name = event.option.id
191
+ if name is not None and name in theme.get_registry():
192
+ try:
193
+ self.app.theme = name
194
+ self._sync_terminal_background()
195
+ # refresh_css only repaints the active (modal) screen's layout;
196
+ # force the screen beneath us to repaint so the user sees the
197
+ # preview through the transparent scrim.
198
+ stack = self.app.screen_stack
199
+ if len(stack) > 1:
200
+ stack[-2].refresh(layout=True)
201
+ except Exception:
202
+ logger.warning("Failed to preview theme '%s'", name, exc_info=True)
203
+ try:
204
+ self.app.theme = self._original_theme
205
+ self._sync_terminal_background()
206
+ except Exception:
207
+ logger.warning(
208
+ "Failed to restore original theme '%s'",
209
+ self._original_theme,
210
+ exc_info=True,
211
+ )
212
+
213
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
214
+ """Commit the selected theme.
215
+
216
+ Args:
217
+ event: The option selected event.
218
+ """
219
+ name = event.option.id
220
+ if name is not None and name in theme.get_registry():
221
+ self.dismiss(name)
222
+ else:
223
+ logger.warning("Selected theme '%s' is no longer available", name)
224
+ self.dismiss(None)
225
+
226
+ def action_cancel(self) -> None:
227
+ """Dismiss, keeping a terminal default chosen this session or restoring.
228
+
229
+ Pressing `t` to save a per-terminal default is a deliberate choice, so
230
+ Esc keeps that theme instead of reverting. `action_set_for_terminal`
231
+ records the choice synchronously (and clears it only if the async save
232
+ fails), so Esc keeps the theme even when the write is still in flight;
233
+ the persisted `[ui.terminal_themes]` mapping is the source of truth
234
+ across sessions. `dismiss(None)` intentionally skips the global
235
+ `[ui].theme` write. Without a `t` press — or after a failed save — Esc
236
+ restores the theme that was active when the picker opened.
237
+ """
238
+ keep = self._session_terminal_default
239
+ if keep is not None:
240
+ self._cancel_kept_terminal_default = keep
241
+ target = keep if keep is not None else self._original_theme
242
+ try:
243
+ self.app.theme = target
244
+ self._sync_terminal_background()
245
+ except Exception:
246
+ # A theme can be unregistered mid-session; never trap the user in
247
+ # the modal. Log and dismiss regardless so Esc always closes.
248
+ logger.warning(
249
+ "Failed to apply theme '%s' on cancel", target, exc_info=True
250
+ )
251
+ self.dismiss(None)
252
+
253
+ def _discard_failed_terminal_default_save(self, name: str) -> None:
254
+ if self._session_terminal_default != name:
255
+ return
256
+ self._session_terminal_default = None
257
+ if self._cancel_kept_terminal_default != name:
258
+ return
259
+ self._cancel_kept_terminal_default = None
260
+ if self.app.theme != name:
261
+ return
262
+ try:
263
+ self.app.theme = self._original_theme
264
+ self._sync_terminal_background()
265
+ except Exception:
266
+ logger.warning(
267
+ "Failed to restore original theme '%s' after terminal save failure",
268
+ self._original_theme,
269
+ exc_info=True,
270
+ )
271
+
272
+ def action_cursor_down(self) -> None:
273
+ """Move the option list cursor down (Tab)."""
274
+ self.query_one(OptionList).action_cursor_down()
275
+
276
+ def action_cursor_up(self) -> None:
277
+ """Move the option list cursor up (Shift+Tab)."""
278
+ self.query_one(OptionList).action_cursor_up()
279
+
280
+ def action_set_for_terminal(self) -> None:
281
+ """Persist the highlighted theme as the default for `TERM_PROGRAM`.
282
+
283
+ Writes `[ui.terminal_themes][TERM_PROGRAM] = name` and updates the
284
+ `(default)` badge in the option list without closing the picker, so
285
+ the user can confirm the change and keep browsing. `[ui].theme` is
286
+ intentionally not touched because this action saves only the current
287
+ terminal default. Config writes are serialized in `app.py`, so
288
+ overlapping global-theme and per-terminal-theme saves cannot clobber
289
+ each other's keys.
290
+
291
+ No-ops with a warning toast if `TERM_PROGRAM` is unset, or silently
292
+ if the option list has no highlighted entry / the highlighted id
293
+ isn't a registered theme.
294
+ """
295
+ term_program = os.environ.get("TERM_PROGRAM", "").strip()
296
+ if not term_program:
297
+ self.app.notify(
298
+ "TERM_PROGRAM is unset; can't set a per-terminal default. "
299
+ "Set the [ui].theme directly with Enter.",
300
+ severity="warning",
301
+ markup=False,
302
+ timeout=6,
303
+ )
304
+ return
305
+
306
+ option_list = self.query_one(OptionList)
307
+ if option_list.highlighted is None:
308
+ logger.warning("action_set_for_terminal invoked with no highlighted option")
309
+ return
310
+ option = option_list.get_option_at_index(option_list.highlighted)
311
+ name = option.id
312
+ if name is None or name not in theme.get_registry():
313
+ logger.warning(
314
+ "action_set_for_terminal got unregistered option id '%s'", name
315
+ )
316
+ return
317
+
318
+ # Record the deliberate choice synchronously so Esc keeps this theme
319
+ # even if the user dismisses before the async write returns (otherwise
320
+ # a slow write would race the cancel path and revert). The failure
321
+ # branches below clear it so a save that errors still reverts on Esc.
322
+ self._session_terminal_default = name
323
+
324
+ async def _persist() -> None:
325
+ try:
326
+ from deepagents_code.app import _save_terminal_theme_mapping_result
327
+
328
+ status = await asyncio.to_thread(
329
+ _save_terminal_theme_mapping_result, term_program, name
330
+ )
331
+ except Exception as exc:
332
+ logger.exception("Failed to persist terminal theme mapping")
333
+ self._discard_failed_terminal_default_save(name)
334
+ self.app.notify(
335
+ f"Could not save terminal mapping ({type(exc).__name__}).",
336
+ severity="error",
337
+ markup=False,
338
+ timeout=6,
339
+ )
340
+ return
341
+ if not status.ok:
342
+ self._discard_failed_terminal_default_save(name)
343
+ self.app.notify(
344
+ status.message or "Could not save terminal mapping.",
345
+ severity=status.severity,
346
+ markup=False,
347
+ timeout=6,
348
+ )
349
+ return
350
+ if status.message is not None:
351
+ self.app.notify(
352
+ status.message,
353
+ severity=status.severity,
354
+ markup=False,
355
+ timeout=6,
356
+ )
357
+ # Update the badge in place if the screen is still mounted.
358
+ # The user may have dismissed the picker (Esc/Enter) while the
359
+ # write was in flight; `is_mounted` guards the widget tree.
360
+ if self.is_mounted:
361
+ self._terminal_default = name
362
+ self._rerender_options()
363
+ self.app.notify(
364
+ f"Set '{name}' as the default for {term_program}.",
365
+ severity="information",
366
+ markup=False,
367
+ timeout=4,
368
+ )
369
+
370
+ # Anchor the worker on the app, not this screen — if the user
371
+ # dismisses the picker mid-flight, the screen tears down its own
372
+ # workers but the write should still complete and toast.
373
+ self.app.run_worker(_persist(), exclusive=False)
374
+
375
+ def action_toggle_names(self) -> None:
376
+ """Toggle between human labels and registry keys in the option list.
377
+
378
+ Useful for copying the canonical key into `[ui.terminal_themes]` or
379
+ `[ui].theme` without leaving the picker.
380
+ """
381
+ self._show_keys = not self._show_keys
382
+ self._rerender_options()
383
+
384
+ def _rerender_options(self) -> None:
385
+ """Rebuild the option list, preserving the cursor position.
386
+
387
+ Used when the badge text or label/key mode changes — Textual's
388
+ `OptionList` doesn't expose a way to mutate a rendered prompt, so
389
+ we recreate the options.
390
+ """
391
+ option_list = self.query_one(OptionList)
392
+ cursor = option_list.highlighted
393
+ registry = theme.get_registry()
394
+ new_options = [
395
+ Option(self._format_option(name, entry), id=name)
396
+ for name, entry in registry.items()
397
+ ]
398
+ option_list.clear_options()
399
+ option_list.add_options(new_options)
400
+ if cursor is not None:
401
+ option_list.highlighted = cursor