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,420 @@
|
|
|
1
|
+
"""Interactive agent selector screen for `/agents` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
9
|
+
|
|
10
|
+
from textual.binding import Binding, BindingType
|
|
11
|
+
from textual.containers import Vertical
|
|
12
|
+
from textual.content import Content
|
|
13
|
+
from textual.screen import ModalScreen
|
|
14
|
+
from textual.widgets import OptionList, Static
|
|
15
|
+
from textual.widgets.option_list import Option
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from textual.app import ComposeResult
|
|
19
|
+
|
|
20
|
+
from deepagents_code import theme
|
|
21
|
+
from deepagents_code.config import Glyphs, get_glyphs, is_ascii_mode
|
|
22
|
+
from deepagents_code.model_config import clear_default_agent, save_default_agent
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentSelectorScreen(ModalScreen[str | None]):
|
|
28
|
+
"""Modal dialog for switching between available agents.
|
|
29
|
+
|
|
30
|
+
Displays agents found in `~/.deepagents/` in an `OptionList`. Returns the
|
|
31
|
+
selected agent name on Enter, or `None` on Esc (no change).
|
|
32
|
+
`Ctrl+S` toggles the highlighted agent as the persisted default
|
|
33
|
+
(`[agents].default`), mirroring the model selector's affordance.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
37
|
+
Binding("escape", "cancel", "Cancel", show=False),
|
|
38
|
+
Binding("tab", "cursor_down", "Next", show=False, priority=True),
|
|
39
|
+
Binding("shift+tab", "cursor_up", "Previous", show=False, priority=True),
|
|
40
|
+
Binding("ctrl+s", "set_default", "Set default", show=False, priority=True),
|
|
41
|
+
]
|
|
42
|
+
"""Key bindings for the selector.
|
|
43
|
+
|
|
44
|
+
Esc dismisses without switching agents. Arrow keys, Enter, and letter
|
|
45
|
+
navigation are handled natively by the embedded `OptionList`; Tab /
|
|
46
|
+
Shift+Tab are bound here to advance the cursor for consistency with
|
|
47
|
+
other selector screens. Ctrl+S toggles the highlighted agent as the
|
|
48
|
+
persisted default.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
CSS = """
|
|
52
|
+
AgentSelectorScreen {
|
|
53
|
+
align: center middle;
|
|
54
|
+
background: transparent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
AgentSelectorScreen > Vertical {
|
|
58
|
+
width: 60;
|
|
59
|
+
max-width: 90%;
|
|
60
|
+
height: auto;
|
|
61
|
+
max-height: 80%;
|
|
62
|
+
background: $surface;
|
|
63
|
+
border: solid $primary;
|
|
64
|
+
padding: 1 2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
AgentSelectorScreen .agent-selector-title {
|
|
68
|
+
text-style: bold;
|
|
69
|
+
color: $primary;
|
|
70
|
+
text-align: center;
|
|
71
|
+
margin-bottom: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
AgentSelectorScreen .agent-selector-subtitle {
|
|
75
|
+
height: auto;
|
|
76
|
+
color: $text-muted;
|
|
77
|
+
text-align: center;
|
|
78
|
+
margin-bottom: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
AgentSelectorScreen OptionList {
|
|
82
|
+
height: auto;
|
|
83
|
+
max-height: 16;
|
|
84
|
+
background: $background;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
AgentSelectorScreen .agent-selector-help {
|
|
88
|
+
height: auto;
|
|
89
|
+
color: $text-muted;
|
|
90
|
+
text-style: italic;
|
|
91
|
+
margin-top: 1;
|
|
92
|
+
text-align: center;
|
|
93
|
+
}
|
|
94
|
+
"""
|
|
95
|
+
"""Styling for the centered modal shell, title, option list, and help footer."""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
current_agent: str | None,
|
|
100
|
+
agent_names: list[str],
|
|
101
|
+
*,
|
|
102
|
+
default_agent: str | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Initialize the `AgentSelectorScreen`.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
current_agent: The name of the currently active agent (to
|
|
108
|
+
highlight).
|
|
109
|
+
|
|
110
|
+
May be `None` when no agent is active.
|
|
111
|
+
agent_names: Sorted list of available agent names to display.
|
|
112
|
+
default_agent: The persisted default agent name from
|
|
113
|
+
`[agents].default`, or `None` if no default is set.
|
|
114
|
+
"""
|
|
115
|
+
super().__init__()
|
|
116
|
+
self._current_agent = current_agent
|
|
117
|
+
self._agent_names = agent_names
|
|
118
|
+
self._default_agent = default_agent
|
|
119
|
+
|
|
120
|
+
def compose(self) -> ComposeResult:
|
|
121
|
+
"""Compose the screen layout.
|
|
122
|
+
|
|
123
|
+
Yields:
|
|
124
|
+
Widgets for the agent selector UI.
|
|
125
|
+
"""
|
|
126
|
+
glyphs = get_glyphs()
|
|
127
|
+
|
|
128
|
+
with Vertical():
|
|
129
|
+
yield Static("Select Agent", classes="agent-selector-title")
|
|
130
|
+
if self._agent_names:
|
|
131
|
+
yield Static(
|
|
132
|
+
"Switching restarts the agent and starts a new thread.",
|
|
133
|
+
classes="agent-selector-subtitle",
|
|
134
|
+
)
|
|
135
|
+
option_list = OptionList(
|
|
136
|
+
*self._build_options(),
|
|
137
|
+
id="agent-options",
|
|
138
|
+
)
|
|
139
|
+
option_list.highlighted = self._current_index()
|
|
140
|
+
yield option_list
|
|
141
|
+
help_text = self._help_text(glyphs)
|
|
142
|
+
else:
|
|
143
|
+
yield Static(
|
|
144
|
+
"No agents found in ~/.deepagents/.\n"
|
|
145
|
+
"Run dcode with -a <name> to create one.",
|
|
146
|
+
classes="agent-selector-help",
|
|
147
|
+
)
|
|
148
|
+
help_text = f"{glyphs.bullet} Esc close"
|
|
149
|
+
yield Static(help_text, classes="agent-selector-help", id="agent-help")
|
|
150
|
+
|
|
151
|
+
def _build_options(self) -> list[Option]:
|
|
152
|
+
"""Build option entries with `(current)` / `(default)` suffixes.
|
|
153
|
+
|
|
154
|
+
Render labels via `Content.from_markup` so agent directory names
|
|
155
|
+
containing Rich markup characters (e.g. `[`) don't break rendering.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
One `Option` per agent name in `self._agent_names`.
|
|
159
|
+
"""
|
|
160
|
+
return [Option(self._format_label(name), id=name) for name in self._agent_names]
|
|
161
|
+
|
|
162
|
+
def _format_label(self, name: str) -> Content:
|
|
163
|
+
"""Render an agent's label with `(current)` / `(default)` markers.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: The agent directory name.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Styled `Content` label.
|
|
170
|
+
"""
|
|
171
|
+
is_current = name == self._current_agent
|
|
172
|
+
is_default = name == self._default_agent
|
|
173
|
+
if is_current and is_default:
|
|
174
|
+
return Content.from_markup(
|
|
175
|
+
"$name [dim](current,[/dim] [bold]default[/bold][dim])[/dim]",
|
|
176
|
+
name=name,
|
|
177
|
+
)
|
|
178
|
+
if is_current:
|
|
179
|
+
return Content.from_markup("$name [dim](current)[/dim]", name=name)
|
|
180
|
+
if is_default:
|
|
181
|
+
return Content.from_markup(
|
|
182
|
+
"$name [dim]([/dim][bold]default[/bold][dim])[/dim]", name=name
|
|
183
|
+
)
|
|
184
|
+
return Content.from_markup("$name", name=name)
|
|
185
|
+
|
|
186
|
+
def _current_index(self) -> int:
|
|
187
|
+
"""Return the index of the current agent in the option list, or 0."""
|
|
188
|
+
if self._current_agent is None:
|
|
189
|
+
return 0
|
|
190
|
+
try:
|
|
191
|
+
return self._agent_names.index(self._current_agent)
|
|
192
|
+
except ValueError:
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _help_text(glyphs: Glyphs) -> str:
|
|
197
|
+
r"""Build the help-line text shown beneath the option list.
|
|
198
|
+
|
|
199
|
+
Split into two balanced rows joined by `\n` so the wrap is
|
|
200
|
+
predictable at the modal's fixed 60-column width — Textual's
|
|
201
|
+
default word-wrap might otherwise break mid-phrase (e.g.,
|
|
202
|
+
between "set" and "default"), which reads as a bug. The Static
|
|
203
|
+
host has `height: auto` and `text-align: center`, so each row
|
|
204
|
+
centers on its own line.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
glyphs: Glyph set for the active terminal mode.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Two-line help string describing the available key bindings.
|
|
211
|
+
"""
|
|
212
|
+
return (
|
|
213
|
+
f"{glyphs.arrow_up}/{glyphs.arrow_down} or Tab switch"
|
|
214
|
+
f" {glyphs.bullet} Enter select\n"
|
|
215
|
+
f"Ctrl+S set default {glyphs.bullet} Esc cancel"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def on_mount(self) -> None:
|
|
219
|
+
"""Apply ASCII border if needed."""
|
|
220
|
+
if is_ascii_mode():
|
|
221
|
+
container = self.query_one(Vertical)
|
|
222
|
+
colors = theme.get_theme_colors(self)
|
|
223
|
+
container.styles.border = ("ascii", colors.success)
|
|
224
|
+
|
|
225
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
226
|
+
"""Dismiss with the selected agent name.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
event: The option selected event.
|
|
230
|
+
"""
|
|
231
|
+
name = event.option.id
|
|
232
|
+
self.dismiss(name)
|
|
233
|
+
|
|
234
|
+
def action_cancel(self) -> None:
|
|
235
|
+
"""Cancel without switching agents."""
|
|
236
|
+
self.dismiss(None)
|
|
237
|
+
|
|
238
|
+
def action_cursor_down(self) -> None:
|
|
239
|
+
"""Move the option list cursor down (Tab)."""
|
|
240
|
+
option_list = self._option_list()
|
|
241
|
+
if option_list is not None:
|
|
242
|
+
option_list.action_cursor_down()
|
|
243
|
+
|
|
244
|
+
def action_cursor_up(self) -> None:
|
|
245
|
+
"""Move the option list cursor up (Shift+Tab)."""
|
|
246
|
+
option_list = self._option_list()
|
|
247
|
+
if option_list is not None:
|
|
248
|
+
option_list.action_cursor_up()
|
|
249
|
+
|
|
250
|
+
async def action_set_default(self) -> None:
|
|
251
|
+
"""Toggle the highlighted agent as the persisted default.
|
|
252
|
+
|
|
253
|
+
If the highlighted agent is already the default, clears it.
|
|
254
|
+
Otherwise sets it as the new default. Disk I/O is offloaded to a
|
|
255
|
+
thread so the modal stays responsive. The help line shows a
|
|
256
|
+
transient confirmation/error message; the option list is rebuilt
|
|
257
|
+
in place so the `(default)` marker tracks the new state.
|
|
258
|
+
|
|
259
|
+
Robustness notes:
|
|
260
|
+
* The `to_thread` call is wrapped in `try/except Exception` so
|
|
261
|
+
an unexpected error inside the persistence functions
|
|
262
|
+
(e.g., `tomli_w.dump` raising a `TypeError`) is treated as
|
|
263
|
+
a normal save failure rather than killing the modal.
|
|
264
|
+
* Post-await `query_one` is guarded against `NoMatches` so a
|
|
265
|
+
user dismissing the modal mid-flight does not surface a
|
|
266
|
+
Textual callback error.
|
|
267
|
+
* The option-list rebuild is staged in `_refresh_options`,
|
|
268
|
+
which builds the new options first and only swaps on
|
|
269
|
+
success. A failure mid-rebuild leaves the existing list
|
|
270
|
+
intact and shows an error in the help line.
|
|
271
|
+
"""
|
|
272
|
+
from textual.css.query import NoMatches
|
|
273
|
+
|
|
274
|
+
option_list = self._option_list()
|
|
275
|
+
if option_list is None or not self._agent_names:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
highlighted = option_list.highlighted
|
|
279
|
+
if highlighted is None or not (0 <= highlighted < len(self._agent_names)):
|
|
280
|
+
return
|
|
281
|
+
name = self._agent_names[highlighted]
|
|
282
|
+
|
|
283
|
+
if name == self._default_agent:
|
|
284
|
+
new_default: str | None = None
|
|
285
|
+
success_msg = "Default cleared"
|
|
286
|
+
failure_msg = "Failed to clear default"
|
|
287
|
+
try:
|
|
288
|
+
ok = await asyncio.to_thread(clear_default_agent)
|
|
289
|
+
except Exception:
|
|
290
|
+
logger.exception("clear_default_agent raised unexpectedly")
|
|
291
|
+
ok = False
|
|
292
|
+
else:
|
|
293
|
+
new_default = name
|
|
294
|
+
success_msg = f"Default set to {name}"
|
|
295
|
+
failure_msg = "Failed to save default"
|
|
296
|
+
try:
|
|
297
|
+
ok = await asyncio.to_thread(save_default_agent, name)
|
|
298
|
+
except Exception:
|
|
299
|
+
logger.exception("save_default_agent raised unexpectedly for %r", name)
|
|
300
|
+
ok = False
|
|
301
|
+
|
|
302
|
+
# The user may have dismissed the modal while the disk I/O was in
|
|
303
|
+
# flight; don't try to mutate widgets on an unmounted screen.
|
|
304
|
+
try:
|
|
305
|
+
help_widget = self.query_one("#agent-help", Static)
|
|
306
|
+
except NoMatches:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if not ok:
|
|
310
|
+
help_widget.update(
|
|
311
|
+
Content.styled(
|
|
312
|
+
failure_msg,
|
|
313
|
+
f"bold {theme.get_theme_colors(self).error}",
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
# Apply the rebuild before mutating `_default_agent` so a failure
|
|
320
|
+
# leaves the picker visually consistent with on-disk state.
|
|
321
|
+
if not self._refresh_options(option_list, highlighted, new_default):
|
|
322
|
+
help_widget.update(
|
|
323
|
+
Content.styled(
|
|
324
|
+
"Failed to refresh agent list",
|
|
325
|
+
f"bold {theme.get_theme_colors(self).error}",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
self._default_agent = new_default
|
|
332
|
+
help_widget.update(Content.styled(success_msg, "bold"))
|
|
333
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
334
|
+
|
|
335
|
+
def _refresh_options(
|
|
336
|
+
self,
|
|
337
|
+
option_list: OptionList,
|
|
338
|
+
highlighted: int,
|
|
339
|
+
new_default: str | None,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Rebuild option labels in place to track the new `(default)` state.
|
|
342
|
+
|
|
343
|
+
Builds the new option list with `new_default` applied first and
|
|
344
|
+
only mutates the live `OptionList` once construction succeeds.
|
|
345
|
+
Failure mid-build leaves the existing list intact, avoiding the
|
|
346
|
+
catastrophic empty-picker state where `clear_options()` had
|
|
347
|
+
succeeded but `add_options(...)` raised.
|
|
348
|
+
|
|
349
|
+
`OptionList.clear_options()` resets the highlight to `None`, so
|
|
350
|
+
restore it explicitly to the previously selected row to avoid
|
|
351
|
+
appearing to lose the cursor after a Ctrl+S toggle.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
option_list: The live `OptionList` to rebuild in place.
|
|
355
|
+
highlighted: Index to restore as the highlighted row.
|
|
356
|
+
new_default: The agent name about to become the default
|
|
357
|
+
(`None` for clear).
|
|
358
|
+
|
|
359
|
+
Passed in rather than read from `self._default_agent`
|
|
360
|
+
so the caller can decide whether to commit the new
|
|
361
|
+
default based on the rebuild's success.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
`True` when the rebuild applied cleanly, `False` if option
|
|
365
|
+
construction or mounting raised. On `False`, the live
|
|
366
|
+
option list has been left untouched.
|
|
367
|
+
"""
|
|
368
|
+
previous_default = self._default_agent
|
|
369
|
+
self._default_agent = new_default
|
|
370
|
+
try:
|
|
371
|
+
new_options = self._build_options()
|
|
372
|
+
except Exception:
|
|
373
|
+
logger.exception("Failed to build new agent picker options")
|
|
374
|
+
self._default_agent = previous_default
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
option_list.clear_options()
|
|
379
|
+
option_list.add_options(new_options)
|
|
380
|
+
except Exception:
|
|
381
|
+
logger.exception("Failed to mount rebuilt agent picker options")
|
|
382
|
+
self._default_agent = previous_default
|
|
383
|
+
# Best-effort restore of the prior options so the user is
|
|
384
|
+
# not left staring at an empty picker.
|
|
385
|
+
with contextlib.suppress(Exception):
|
|
386
|
+
option_list.clear_options()
|
|
387
|
+
option_list.add_options(self._build_options())
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
if 0 <= highlighted < len(self._agent_names):
|
|
391
|
+
option_list.highlighted = highlighted
|
|
392
|
+
# `_default_agent` is set on entry so `_build_options` reflects
|
|
393
|
+
# the new state; revert it here so the caller can commit only
|
|
394
|
+
# after the rebuild has fully succeeded.
|
|
395
|
+
self._default_agent = previous_default
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
def _restore_help_text(self) -> None:
|
|
399
|
+
"""Restore the default help text after a transient message.
|
|
400
|
+
|
|
401
|
+
Guarded against the user having dismissed the modal during the
|
|
402
|
+
3-second timer; without the guard, `query_one` would raise
|
|
403
|
+
`NoMatches` and Textual would surface it as a callback error.
|
|
404
|
+
"""
|
|
405
|
+
from textual.css.query import NoMatches
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
help_widget = self.query_one("#agent-help", Static)
|
|
409
|
+
except NoMatches:
|
|
410
|
+
return
|
|
411
|
+
help_widget.update(self._help_text(get_glyphs()))
|
|
412
|
+
|
|
413
|
+
def _option_list(self) -> OptionList | None:
|
|
414
|
+
"""Return the agent `OptionList`, or `None` when the screen is empty."""
|
|
415
|
+
from textual.css.query import NoMatches
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
return self.query_one("#agent-options", OptionList)
|
|
419
|
+
except NoMatches:
|
|
420
|
+
return None
|