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,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
|