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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- 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
|