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,781 @@
|
|
|
1
|
+
"""Status bar widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal, get_args
|
|
9
|
+
|
|
10
|
+
from textual.containers import Horizontal
|
|
11
|
+
from textual.content import Content
|
|
12
|
+
from textual.css.query import NoMatches
|
|
13
|
+
from textual.reactive import reactive
|
|
14
|
+
from textual.widget import Widget
|
|
15
|
+
from textual.widgets import Static
|
|
16
|
+
|
|
17
|
+
from deepagents_code._constants import FIREWORKS_MODEL_ID_PREFIXES
|
|
18
|
+
from deepagents_code._env_vars import HIDE_CWD, HIDE_GIT_BRANCH, is_env_truthy
|
|
19
|
+
from deepagents_code.config import get_glyphs
|
|
20
|
+
from deepagents_code.tui.widgets.loading import Spinner
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from textual import events
|
|
26
|
+
from textual.app import ComposeResult, RenderResult
|
|
27
|
+
from textual.geometry import Size
|
|
28
|
+
from textual.timer import Timer
|
|
29
|
+
|
|
30
|
+
PROVIDER_PREFIX_STRIPS: dict[str, tuple[str, ...]] = {
|
|
31
|
+
"fireworks": FIREWORKS_MODEL_ID_PREFIXES,
|
|
32
|
+
}
|
|
33
|
+
"""Some providers (e.g. Fireworks) require fully-qualified IDs like
|
|
34
|
+
`accounts/fireworks/models/...` or `accounts/fireworks/routers/...` that crowd
|
|
35
|
+
out the rest of the status bar; strip the registered prefixes before display."""
|
|
36
|
+
|
|
37
|
+
ConnectionState = Literal["", "connecting", "reconnecting", "resuming"]
|
|
38
|
+
"""Connection states the status bar can display (`''` means cleared)."""
|
|
39
|
+
|
|
40
|
+
CONNECTION_STATES = frozenset(get_args(ConnectionState))
|
|
41
|
+
"""Runtime view of `ConnectionState` for `set_connection`'s defensive guard.
|
|
42
|
+
|
|
43
|
+
Derived from the `Literal` so the two can never drift."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ModelLabel(Widget):
|
|
47
|
+
"""A label that displays a model name, right-aligned with smart truncation.
|
|
48
|
+
|
|
49
|
+
When the full `provider:model` text doesn't fit, the provider is dropped
|
|
50
|
+
first. If the bare model name still doesn't fit, it is left-truncated
|
|
51
|
+
with a leading ellipsis so the most distinctive tail stays visible.
|
|
52
|
+
|
|
53
|
+
When a reasoning effort is set, its label is appended to the model and
|
|
54
|
+
participates in the same ladder: the effort suffix is preserved (with the
|
|
55
|
+
model left-truncated to make room) and is only dropped once even the
|
|
56
|
+
left-truncated model plus effort cannot fit.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
provider: reactive[str] = reactive("", layout=True)
|
|
60
|
+
model: reactive[str] = reactive("", layout=True)
|
|
61
|
+
effort: reactive[str] = reactive("", layout=True)
|
|
62
|
+
|
|
63
|
+
def _clean_model(self) -> str:
|
|
64
|
+
"""Strip the provider's registered prefix so the status bar stays compact.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Model name with the provider's registered prefix removed if present,
|
|
68
|
+
otherwise the original name.
|
|
69
|
+
"""
|
|
70
|
+
name = self.model
|
|
71
|
+
if not name or not self.provider:
|
|
72
|
+
return name
|
|
73
|
+
# Match on normalized text but slice the original to preserve its casing.
|
|
74
|
+
name_lower = name.lower()
|
|
75
|
+
for prefix in PROVIDER_PREFIX_STRIPS.get(self.provider, ()):
|
|
76
|
+
if name_lower.startswith(prefix):
|
|
77
|
+
return name[len(prefix) :]
|
|
78
|
+
return name
|
|
79
|
+
|
|
80
|
+
def _with_effort(self, text: str) -> str:
|
|
81
|
+
"""Append the reasoning effort label when one is set.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
text: Base model display text.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Model display text with the effort suffix (a per-session override or
|
|
88
|
+
the provider default) when one is present, else
|
|
89
|
+
`text` unchanged.
|
|
90
|
+
"""
|
|
91
|
+
return f"{text} {self.effort}" if self.effort else text
|
|
92
|
+
|
|
93
|
+
def get_content_width(self, container: Size, viewport: Size) -> int: # noqa: ARG002
|
|
94
|
+
"""Return the intrinsic width so `width: auto` works.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
container: Size of the container.
|
|
98
|
+
viewport: Size of the viewport.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Character length of the full provider:model string.
|
|
102
|
+
"""
|
|
103
|
+
if not self.model:
|
|
104
|
+
return 0
|
|
105
|
+
model = self._clean_model()
|
|
106
|
+
full = f"{self.provider}:{model}" if self.provider else model
|
|
107
|
+
return len(self._with_effort(full))
|
|
108
|
+
|
|
109
|
+
def render(self) -> RenderResult:
|
|
110
|
+
"""Render the model label with width-aware truncation.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Text content, truncated from the left when necessary.
|
|
114
|
+
"""
|
|
115
|
+
width = self.content_size.width
|
|
116
|
+
if not self.model or width <= 0:
|
|
117
|
+
return ""
|
|
118
|
+
model = self._clean_model()
|
|
119
|
+
full = f"{self.provider}:{model}" if self.provider else model
|
|
120
|
+
full_with_effort = self._with_effort(full)
|
|
121
|
+
model_with_effort = self._with_effort(model)
|
|
122
|
+
if len(full_with_effort) <= width:
|
|
123
|
+
return Content(full_with_effort)
|
|
124
|
+
if len(model_with_effort) <= width:
|
|
125
|
+
return Content(model_with_effort)
|
|
126
|
+
if self.effort and width > len(self.effort) + 2:
|
|
127
|
+
model_width = width - len(self.effort) - 1
|
|
128
|
+
return Content(f"\u2026{model[-(model_width - 1) :]} {self.effort}")
|
|
129
|
+
if len(model) <= width:
|
|
130
|
+
return Content(model)
|
|
131
|
+
if width > 1:
|
|
132
|
+
return Content("\u2026" + model[-(width - 1) :])
|
|
133
|
+
return Content("\u2026")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class BranchLabel(Widget):
|
|
137
|
+
"""A label that displays the git branch with glyph-aware truncation.
|
|
138
|
+
|
|
139
|
+
Unlike CSS `text-overflow: ellipsis` (which always uses the Unicode
|
|
140
|
+
ellipsis character), this widget truncates manually in :meth:`render` using
|
|
141
|
+
:func:`get_glyphs` so ASCII mode (`DEEPAGENTS_CODE_UI_CHARSET_MODE=ascii`)
|
|
142
|
+
gets `"..."` instead of `"…"`.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
branch: reactive[str] = reactive("", layout=True)
|
|
146
|
+
|
|
147
|
+
def get_content_width(self, container: Size, viewport: Size) -> int: # noqa: ARG002
|
|
148
|
+
"""Return the intrinsic width so the widget participates in flex layout.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
container: Size of the container.
|
|
152
|
+
viewport: Size of the viewport.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Character length of the full branch string (icon + space + name),
|
|
156
|
+
or `0` when the branch is empty.
|
|
157
|
+
"""
|
|
158
|
+
if not self.branch:
|
|
159
|
+
return 0
|
|
160
|
+
icon = get_glyphs().git_branch
|
|
161
|
+
return len(icon) + 1 + len(self.branch)
|
|
162
|
+
|
|
163
|
+
def render(self) -> RenderResult:
|
|
164
|
+
"""Render the branch label, truncating with the configured glyph.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Branch text (icon + name) truncated from the right with
|
|
168
|
+
:func:`get_glyphs`'s ellipsis when it overflows the available
|
|
169
|
+
width, or an empty string when no branch is set.
|
|
170
|
+
"""
|
|
171
|
+
width = self.content_size.width
|
|
172
|
+
if not self.branch or width <= 0:
|
|
173
|
+
return ""
|
|
174
|
+
icon = get_glyphs().git_branch
|
|
175
|
+
full = f"{icon} {self.branch}"
|
|
176
|
+
if len(full) <= width:
|
|
177
|
+
return full
|
|
178
|
+
ellipsis = get_glyphs().ellipsis
|
|
179
|
+
if width <= len(ellipsis):
|
|
180
|
+
return full[:width]
|
|
181
|
+
return full[: width - len(ellipsis)] + ellipsis
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class StatusBar(Horizontal):
|
|
185
|
+
"""Status bar showing mode, auto-approve, cwd, git branch, tokens, and model."""
|
|
186
|
+
|
|
187
|
+
DEFAULT_CSS = """
|
|
188
|
+
StatusBar {
|
|
189
|
+
height: 1;
|
|
190
|
+
dock: bottom;
|
|
191
|
+
background: $background;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
StatusBar .status-mode {
|
|
195
|
+
width: auto;
|
|
196
|
+
padding: 0 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
StatusBar .status-mode.normal {
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
StatusBar .status-mode.shell {
|
|
204
|
+
background: $mode-bash;
|
|
205
|
+
color: white;
|
|
206
|
+
text-style: bold;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
StatusBar .status-mode.command {
|
|
210
|
+
background: $mode-command;
|
|
211
|
+
color: white;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
StatusBar .status-mode.shell-incognito {
|
|
215
|
+
background: $mode-incognito;
|
|
216
|
+
color: $background;
|
|
217
|
+
text-style: bold;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
StatusBar .status-auto-approve {
|
|
221
|
+
width: auto;
|
|
222
|
+
padding: 0 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
StatusBar .status-auto-approve.on {
|
|
226
|
+
background: $success;
|
|
227
|
+
color: $background;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
StatusBar .status-auto-approve.off {
|
|
231
|
+
background: $warning;
|
|
232
|
+
color: $background;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
StatusBar .status-connection {
|
|
236
|
+
width: auto;
|
|
237
|
+
padding: 0 1;
|
|
238
|
+
color: $warning;
|
|
239
|
+
text-style: bold;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
StatusBar .status-message {
|
|
243
|
+
width: auto;
|
|
244
|
+
padding: 0 1;
|
|
245
|
+
color: $text-muted;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
StatusBar .status-message.thinking {
|
|
249
|
+
color: $warning;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
StatusBar .status-cwd {
|
|
253
|
+
width: auto;
|
|
254
|
+
text-align: right;
|
|
255
|
+
color: $text-muted;
|
|
256
|
+
/* Own both adjacent gaps so they disappear with the cwd: the left gap
|
|
257
|
+
separates it from the auto-approve pill when transient status slots
|
|
258
|
+
are hidden, and the right gap separates it from the branch. */
|
|
259
|
+
padding: 0 1 0 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
StatusBar .status-branch {
|
|
263
|
+
width: 1fr;
|
|
264
|
+
min-width: 0;
|
|
265
|
+
overflow-x: hidden;
|
|
266
|
+
text-wrap: nowrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
StatusBar .status-left-collapsible {
|
|
270
|
+
width: 1fr;
|
|
271
|
+
min-width: 0;
|
|
272
|
+
height: 1;
|
|
273
|
+
overflow-x: hidden;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
StatusBar .status-tokens {
|
|
277
|
+
width: auto;
|
|
278
|
+
padding: 0 1;
|
|
279
|
+
color: $text-muted;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
StatusBar .status-rubric {
|
|
283
|
+
width: auto;
|
|
284
|
+
padding: 0 1;
|
|
285
|
+
color: $success;
|
|
286
|
+
text-style: bold;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
StatusBar ModelLabel {
|
|
290
|
+
width: auto;
|
|
291
|
+
padding: 0 2;
|
|
292
|
+
color: $text-muted;
|
|
293
|
+
text-align: right;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
StatusBar BranchLabel {
|
|
297
|
+
color: $text-muted;
|
|
298
|
+
/* No left pad while cwd is visible: the cwd owns that gap. */
|
|
299
|
+
padding: 0 1 0 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
StatusBar .status-cwd.after-status {
|
|
303
|
+
padding: 0 1 0 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
StatusBar BranchLabel.cwd-hidden {
|
|
307
|
+
/* When cwd is hidden, the branch needs the same left separator that
|
|
308
|
+
cwd normally provides after the auto-approve pill. */
|
|
309
|
+
padding: 0 1 0 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
StatusBar BranchLabel.cwd-hidden.after-status {
|
|
313
|
+
padding: 0 1 0 0;
|
|
314
|
+
}
|
|
315
|
+
"""
|
|
316
|
+
"""Mode badges and auto-approve pills use distinct colors for at-a-glance status."""
|
|
317
|
+
|
|
318
|
+
mode: reactive[str] = reactive("normal", init=False)
|
|
319
|
+
status_message: reactive[str] = reactive("", init=False)
|
|
320
|
+
connection_state: reactive[ConnectionState] = reactive("", init=False)
|
|
321
|
+
queued_count: reactive[int] = reactive(0, init=False)
|
|
322
|
+
auto_approve: reactive[bool] = reactive(default=False, init=False)
|
|
323
|
+
cwd: reactive[str] = reactive("", init=False)
|
|
324
|
+
branch: reactive[str] = reactive("", init=False)
|
|
325
|
+
tokens: reactive[int] = reactive(0, init=False)
|
|
326
|
+
rubric_label: reactive[str] = reactive("", init=False)
|
|
327
|
+
|
|
328
|
+
def __init__(self, cwd: str | Path | None = None, **kwargs: Any) -> None:
|
|
329
|
+
"""Initialize the status bar.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
cwd: Current working directory to display
|
|
333
|
+
**kwargs: Additional arguments passed to parent
|
|
334
|
+
"""
|
|
335
|
+
super().__init__(**kwargs)
|
|
336
|
+
# Store initial cwd - will be used in compose()
|
|
337
|
+
self._initial_cwd = str(cwd) if cwd else str(Path.cwd())
|
|
338
|
+
self._hide_cwd = is_env_truthy(HIDE_CWD)
|
|
339
|
+
self._hide_git_branch = is_env_truthy(HIDE_GIT_BRANCH)
|
|
340
|
+
self._spinner = Spinner()
|
|
341
|
+
self._spinner_timer: Timer | None = None
|
|
342
|
+
self._busy_message = ""
|
|
343
|
+
|
|
344
|
+
def compose(self) -> ComposeResult: # noqa: PLR6301 — Textual widget method
|
|
345
|
+
"""Compose the status bar layout.
|
|
346
|
+
|
|
347
|
+
Yields:
|
|
348
|
+
Widgets for mode, auto-approve, message, cwd, branch, tokens, and
|
|
349
|
+
model display.
|
|
350
|
+
"""
|
|
351
|
+
yield Static("", classes="status-mode normal", id="mode-indicator")
|
|
352
|
+
yield Static(
|
|
353
|
+
"manual",
|
|
354
|
+
classes="status-auto-approve off",
|
|
355
|
+
id="auto-approve-indicator",
|
|
356
|
+
)
|
|
357
|
+
with Horizontal(classes="status-left-collapsible"):
|
|
358
|
+
yield Static("", classes="status-connection", id="connection-indicator")
|
|
359
|
+
yield Static("", classes="status-message", id="status-message")
|
|
360
|
+
yield Static("", classes="status-cwd", id="cwd-display")
|
|
361
|
+
yield BranchLabel(classes="status-branch", id="branch-display")
|
|
362
|
+
yield Static("", classes="status-rubric", id="rubric-display")
|
|
363
|
+
yield Static("", classes="status-tokens", id="tokens-display")
|
|
364
|
+
yield ModelLabel(id="model-display")
|
|
365
|
+
|
|
366
|
+
_CWD_WIDTH_THRESHOLD = 70
|
|
367
|
+
"""Hide cwd display below this terminal width."""
|
|
368
|
+
|
|
369
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
370
|
+
"""Hide the cwd on very narrow terminals.
|
|
371
|
+
|
|
372
|
+
The git branch stays visible at any width (unless disabled via
|
|
373
|
+
`HIDE_GIT_BRANCH`) and ellipsizes to fit; only the cwd is dropped
|
|
374
|
+
outright to reclaim space when the terminal gets narrow.
|
|
375
|
+
"""
|
|
376
|
+
width = event.size.width
|
|
377
|
+
self._set_cwd_visible(not self._hide_cwd and width >= self._CWD_WIDTH_THRESHOLD)
|
|
378
|
+
|
|
379
|
+
def _set_cwd_visible(self, visible: bool) -> None:
|
|
380
|
+
"""Show or hide cwd and keep adjacent branch spacing in sync."""
|
|
381
|
+
with suppress(NoMatches):
|
|
382
|
+
self.query_one("#cwd-display", Static).display = visible
|
|
383
|
+
with suppress(NoMatches):
|
|
384
|
+
branch = self.query_one("#branch-display", BranchLabel)
|
|
385
|
+
if visible:
|
|
386
|
+
branch.remove_class("cwd-hidden")
|
|
387
|
+
else:
|
|
388
|
+
branch.add_class("cwd-hidden")
|
|
389
|
+
self._sync_left_separator()
|
|
390
|
+
|
|
391
|
+
def _sync_left_separator(self) -> None:
|
|
392
|
+
"""Use one separator between the last transient status item and cwd/branch."""
|
|
393
|
+
preceded_by_status = False
|
|
394
|
+
with suppress(NoMatches):
|
|
395
|
+
preceded_by_status = bool(
|
|
396
|
+
self.query_one("#connection-indicator", Static).display
|
|
397
|
+
or self.query_one("#status-message", Static).display
|
|
398
|
+
)
|
|
399
|
+
for selector, widget_type in (
|
|
400
|
+
("#cwd-display", Static),
|
|
401
|
+
("#branch-display", BranchLabel),
|
|
402
|
+
):
|
|
403
|
+
with suppress(NoMatches):
|
|
404
|
+
widget = self.query_one(selector, widget_type)
|
|
405
|
+
if preceded_by_status:
|
|
406
|
+
widget.add_class("after-status")
|
|
407
|
+
else:
|
|
408
|
+
widget.remove_class("after-status")
|
|
409
|
+
|
|
410
|
+
def on_unmount(self) -> None:
|
|
411
|
+
"""Stop the spinner timer so it can't tick on a detached widget."""
|
|
412
|
+
self._stop_spinner()
|
|
413
|
+
|
|
414
|
+
def on_mount(self) -> None:
|
|
415
|
+
"""Set reactive values after mount to trigger watchers safely."""
|
|
416
|
+
from deepagents_code.config import settings
|
|
417
|
+
|
|
418
|
+
self.cwd = self._initial_cwd
|
|
419
|
+
if self._hide_cwd:
|
|
420
|
+
self._set_cwd_visible(False)
|
|
421
|
+
if self._hide_git_branch:
|
|
422
|
+
with suppress(NoMatches):
|
|
423
|
+
self.query_one("#branch-display", BranchLabel).display = False
|
|
424
|
+
# Set initial model display
|
|
425
|
+
label = self.query_one("#model-display", ModelLabel)
|
|
426
|
+
label.provider = settings.model_provider or ""
|
|
427
|
+
label.model = settings.model_name or ""
|
|
428
|
+
with suppress(NoMatches):
|
|
429
|
+
self.query_one("#rubric-display", Static).display = False
|
|
430
|
+
# Reactives are `init=False`, so the connection watcher never fires on
|
|
431
|
+
# mount; render once to hide the empty indicator (and its padding).
|
|
432
|
+
self._render_connection()
|
|
433
|
+
# Same reasoning for the message and token slots: both start empty, so
|
|
434
|
+
# hide them on mount so their padding doesn't reserve a blank gap.
|
|
435
|
+
self.watch_status_message(self.status_message)
|
|
436
|
+
with suppress(NoMatches):
|
|
437
|
+
self.query_one("#tokens-display", Static).display = False
|
|
438
|
+
|
|
439
|
+
def watch_mode(self, mode: str) -> None:
|
|
440
|
+
"""Update mode indicator when mode changes."""
|
|
441
|
+
try:
|
|
442
|
+
indicator = self.query_one("#mode-indicator", Static)
|
|
443
|
+
except NoMatches:
|
|
444
|
+
return
|
|
445
|
+
indicator.remove_class("normal", "shell", "command", "shell-incognito")
|
|
446
|
+
|
|
447
|
+
if mode == "shell":
|
|
448
|
+
indicator.update("SHELL")
|
|
449
|
+
indicator.add_class("shell")
|
|
450
|
+
elif mode == "shell_incognito":
|
|
451
|
+
indicator.update("SHELL")
|
|
452
|
+
indicator.add_class("shell-incognito")
|
|
453
|
+
elif mode == "command":
|
|
454
|
+
indicator.update("CMD")
|
|
455
|
+
indicator.add_class("command")
|
|
456
|
+
else:
|
|
457
|
+
indicator.update("")
|
|
458
|
+
indicator.add_class("normal")
|
|
459
|
+
|
|
460
|
+
def watch_auto_approve(self, new_value: bool) -> None:
|
|
461
|
+
"""Update auto-approve indicator when state changes."""
|
|
462
|
+
try:
|
|
463
|
+
indicator = self.query_one("#auto-approve-indicator", Static)
|
|
464
|
+
except NoMatches:
|
|
465
|
+
return
|
|
466
|
+
indicator.remove_class("on", "off")
|
|
467
|
+
|
|
468
|
+
if new_value:
|
|
469
|
+
indicator.update("auto")
|
|
470
|
+
indicator.add_class("on")
|
|
471
|
+
else:
|
|
472
|
+
indicator.update("manual")
|
|
473
|
+
indicator.add_class("off")
|
|
474
|
+
|
|
475
|
+
def watch_cwd(self, new_value: str) -> None:
|
|
476
|
+
"""Update cwd display when it changes."""
|
|
477
|
+
try:
|
|
478
|
+
display = self.query_one("#cwd-display", Static)
|
|
479
|
+
except NoMatches:
|
|
480
|
+
return
|
|
481
|
+
display.update(self._format_cwd(new_value))
|
|
482
|
+
|
|
483
|
+
def watch_branch(self, new_value: str) -> None:
|
|
484
|
+
"""Update branch display when it changes."""
|
|
485
|
+
try:
|
|
486
|
+
display = self.query_one("#branch-display", BranchLabel)
|
|
487
|
+
except NoMatches:
|
|
488
|
+
return
|
|
489
|
+
display.branch = new_value
|
|
490
|
+
|
|
491
|
+
def watch_status_message(self, new_value: str) -> None:
|
|
492
|
+
"""Update status message display."""
|
|
493
|
+
if self._busy_message:
|
|
494
|
+
# The busy indicator owns the status-message slot while active;
|
|
495
|
+
# defer regular status updates until `set_busy("")` clears it.
|
|
496
|
+
return
|
|
497
|
+
try:
|
|
498
|
+
msg_widget = self.query_one("#status-message", Static)
|
|
499
|
+
except NoMatches:
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
msg_widget.remove_class("thinking")
|
|
503
|
+
# Hide when empty so the widget's padding doesn't reserve a blank gap
|
|
504
|
+
# in the footer (mirrors the connection indicator).
|
|
505
|
+
msg_widget.display = bool(new_value)
|
|
506
|
+
if new_value:
|
|
507
|
+
msg_widget.update(new_value)
|
|
508
|
+
if "thinking" in new_value.lower() or "executing" in new_value.lower():
|
|
509
|
+
msg_widget.add_class("thinking")
|
|
510
|
+
else:
|
|
511
|
+
msg_widget.update("")
|
|
512
|
+
self._sync_left_separator()
|
|
513
|
+
|
|
514
|
+
def watch_connection_state(self, _new_value: ConnectionState) -> None:
|
|
515
|
+
"""Start or stop the spinner and re-render when connection state changes."""
|
|
516
|
+
self._sync_spinner()
|
|
517
|
+
self._render_connection()
|
|
518
|
+
|
|
519
|
+
def watch_queued_count(self, _new_value: int) -> None:
|
|
520
|
+
"""Re-render the connection indicator when the queued count changes."""
|
|
521
|
+
self._render_connection()
|
|
522
|
+
|
|
523
|
+
def _spinner_active(self) -> bool:
|
|
524
|
+
"""Whether any indicator (connection or busy) needs the shared spinner.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
`True` when a connection state or a busy message is active.
|
|
528
|
+
"""
|
|
529
|
+
return bool(self.connection_state) or bool(self._busy_message)
|
|
530
|
+
|
|
531
|
+
def _sync_spinner(self) -> None:
|
|
532
|
+
"""Start or stop the shared spinner to match connection/busy state."""
|
|
533
|
+
if self._spinner_active():
|
|
534
|
+
self._start_spinner()
|
|
535
|
+
else:
|
|
536
|
+
self._stop_spinner()
|
|
537
|
+
|
|
538
|
+
def _start_spinner(self) -> None:
|
|
539
|
+
"""Begin cycling the shared spinner frames.
|
|
540
|
+
|
|
541
|
+
No-op when not yet running (e.g. before mount) since `set_interval`
|
|
542
|
+
requires a live event loop, or when an animation is already active.
|
|
543
|
+
"""
|
|
544
|
+
if self._spinner_timer is not None or not self._running:
|
|
545
|
+
return
|
|
546
|
+
# 0.1s mirrors LoadingWidget so this spinner ticks in step with the
|
|
547
|
+
# in-thread "Thinking" spinner.
|
|
548
|
+
self._spinner_timer = self.set_interval(0.1, self._tick_spinner)
|
|
549
|
+
|
|
550
|
+
def _stop_spinner(self) -> None:
|
|
551
|
+
"""Stop the spinner animation and reset to the first frame."""
|
|
552
|
+
if self._spinner_timer is not None:
|
|
553
|
+
self._spinner_timer.stop()
|
|
554
|
+
self._spinner_timer = None
|
|
555
|
+
self._spinner = Spinner()
|
|
556
|
+
|
|
557
|
+
def _tick_spinner(self) -> None:
|
|
558
|
+
"""Advance the spinner frame and re-render the animated indicators."""
|
|
559
|
+
self._spinner.next_frame()
|
|
560
|
+
self._render_connection()
|
|
561
|
+
self._render_busy()
|
|
562
|
+
|
|
563
|
+
def _render_connection(self) -> None:
|
|
564
|
+
"""Render the combined connection + queued-count indicator text."""
|
|
565
|
+
try:
|
|
566
|
+
widget = self.query_one("#connection-indicator", Static)
|
|
567
|
+
except NoMatches:
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
parts: list[str] = []
|
|
571
|
+
if self.connection_state == "reconnecting":
|
|
572
|
+
parts.append(f"{self._spinner.current_frame()} Reconnecting")
|
|
573
|
+
elif self.connection_state == "resuming":
|
|
574
|
+
parts.append(f"{self._spinner.current_frame()} Resuming")
|
|
575
|
+
elif self.connection_state == "connecting":
|
|
576
|
+
parts.append(f"{self._spinner.current_frame()} Connecting")
|
|
577
|
+
if self.queued_count > 0:
|
|
578
|
+
label = "message" if self.queued_count == 1 else "messages"
|
|
579
|
+
parts.append(f"{self.queued_count} {label} queued")
|
|
580
|
+
separator = f" {get_glyphs().bullet} "
|
|
581
|
+
text = separator.join(parts)
|
|
582
|
+
# Hide the widget entirely when empty so its `padding: 0 1` doesn't
|
|
583
|
+
# leave a 2-column gap between the auto-approve pill and the cwd.
|
|
584
|
+
widget.display = bool(text)
|
|
585
|
+
widget.update(text)
|
|
586
|
+
self._sync_left_separator()
|
|
587
|
+
|
|
588
|
+
def _render_busy(self) -> None:
|
|
589
|
+
"""Render the animated busy indicator into the status-message slot."""
|
|
590
|
+
if not self._busy_message:
|
|
591
|
+
return
|
|
592
|
+
try:
|
|
593
|
+
widget = self.query_one("#status-message", Static)
|
|
594
|
+
except NoMatches:
|
|
595
|
+
return
|
|
596
|
+
widget.remove_class("thinking")
|
|
597
|
+
widget.display = True
|
|
598
|
+
frame = self._spinner.current_frame()
|
|
599
|
+
widget.update(Content.assemble(frame, " ", Content(self._busy_message)))
|
|
600
|
+
self._sync_left_separator()
|
|
601
|
+
|
|
602
|
+
def set_busy(self, message: str) -> None:
|
|
603
|
+
"""Show or clear an animated busy indicator in the status-message slot.
|
|
604
|
+
|
|
605
|
+
Reuses the shared status-bar spinner so heavier UI operations (e.g. a
|
|
606
|
+
model switch that imports a provider package) show activity instead of
|
|
607
|
+
appearing to hang.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
message: Busy text to animate with a spinner, or empty string to
|
|
611
|
+
clear it and restore the regular status message.
|
|
612
|
+
"""
|
|
613
|
+
self._busy_message = message
|
|
614
|
+
self._sync_spinner()
|
|
615
|
+
if message:
|
|
616
|
+
self._render_busy()
|
|
617
|
+
else:
|
|
618
|
+
self.watch_status_message(self.status_message)
|
|
619
|
+
|
|
620
|
+
def set_connection(self, state: ConnectionState) -> None:
|
|
621
|
+
"""Set the connection indicator state.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
state: One of `''` (clear), `'connecting'`, `'reconnecting'`, or
|
|
625
|
+
`'resuming'`.
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
ValueError: If `state` is not a recognized connection state.
|
|
629
|
+
"""
|
|
630
|
+
if state not in CONNECTION_STATES:
|
|
631
|
+
msg = f"Unknown connection state: {state!r}"
|
|
632
|
+
raise ValueError(msg)
|
|
633
|
+
self.connection_state = state
|
|
634
|
+
|
|
635
|
+
def set_queued(self, count: int) -> None:
|
|
636
|
+
"""Set the number of messages waiting in the queue.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
count: Count of queued messages (negative values clamp to `0`).
|
|
640
|
+
"""
|
|
641
|
+
self.queued_count = max(count, 0)
|
|
642
|
+
|
|
643
|
+
def _format_cwd(self, cwd_path: str = "") -> str:
|
|
644
|
+
"""Format the current working directory for display.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Formatted path string, using ~ for home directory when possible.
|
|
648
|
+
"""
|
|
649
|
+
path = Path(cwd_path or self.cwd or self._initial_cwd)
|
|
650
|
+
try:
|
|
651
|
+
# Try to use ~ for home directory
|
|
652
|
+
home = Path.home()
|
|
653
|
+
if path.is_relative_to(home):
|
|
654
|
+
return "~/" + path.relative_to(home).as_posix()
|
|
655
|
+
except (ValueError, RuntimeError):
|
|
656
|
+
pass
|
|
657
|
+
return str(path)
|
|
658
|
+
|
|
659
|
+
def set_mode(self, mode: str) -> None:
|
|
660
|
+
"""Set the current input mode.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
mode: One of "normal", "shell", or "command"
|
|
664
|
+
"""
|
|
665
|
+
self.mode = mode
|
|
666
|
+
|
|
667
|
+
def set_auto_approve(self, *, enabled: bool) -> None:
|
|
668
|
+
"""Set the auto-approve state.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
enabled: Whether auto-approve is enabled
|
|
672
|
+
"""
|
|
673
|
+
self.auto_approve = enabled
|
|
674
|
+
|
|
675
|
+
def set_status_message(self, message: str) -> None:
|
|
676
|
+
"""Set the status message.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
message: Status message to display (empty string to clear)
|
|
680
|
+
"""
|
|
681
|
+
self.status_message = message
|
|
682
|
+
|
|
683
|
+
_approximate: bool = False
|
|
684
|
+
"""Append "+" to the token count to signal that the displayed value is stale.
|
|
685
|
+
|
|
686
|
+
(The actual context is larger because the generation was interrupted before
|
|
687
|
+
the model reported final usage.)
|
|
688
|
+
"""
|
|
689
|
+
_has_token_count: bool = False
|
|
690
|
+
"""Whether the status bar has displayed a real token count this session."""
|
|
691
|
+
|
|
692
|
+
def watch_tokens(self, new_value: int) -> None:
|
|
693
|
+
"""Update token display when count changes."""
|
|
694
|
+
self._render_tokens(new_value, approximate=self._approximate)
|
|
695
|
+
|
|
696
|
+
def watch_rubric_label(self, new_value: str) -> None:
|
|
697
|
+
"""Update rubric display when active rubric state changes."""
|
|
698
|
+
try:
|
|
699
|
+
display = self.query_one("#rubric-display", Static)
|
|
700
|
+
except NoMatches:
|
|
701
|
+
return
|
|
702
|
+
display.display = bool(new_value)
|
|
703
|
+
display.update(new_value)
|
|
704
|
+
|
|
705
|
+
def _render_tokens(self, count: int, *, approximate: bool = False) -> None:
|
|
706
|
+
"""Render the token count into the display widget.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
count: Total context token count.
|
|
710
|
+
approximate: Append "+" suffix to indicate the count is stale
|
|
711
|
+
(e.g. after an interrupted generation).
|
|
712
|
+
"""
|
|
713
|
+
try:
|
|
714
|
+
display = self.query_one("#tokens-display", Static)
|
|
715
|
+
except NoMatches:
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
# Hide when empty so the widget's padding doesn't reserve a blank gap.
|
|
719
|
+
display.display = count > 0
|
|
720
|
+
if count > 0:
|
|
721
|
+
suffix = "+" if approximate else ""
|
|
722
|
+
# Format with K suffix for thousands
|
|
723
|
+
if count >= 1000: # noqa: PLR2004 # Count formatting threshold
|
|
724
|
+
display.update(f"{count / 1000:.1f}K{suffix} tokens")
|
|
725
|
+
else:
|
|
726
|
+
display.update(f"{count}{suffix} tokens")
|
|
727
|
+
else:
|
|
728
|
+
display.update("")
|
|
729
|
+
|
|
730
|
+
def set_rubric_label(self, label: str) -> None:
|
|
731
|
+
"""Set the rubric status label.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
label: Label to display, or empty string to hide the badge.
|
|
735
|
+
"""
|
|
736
|
+
self.rubric_label = label
|
|
737
|
+
|
|
738
|
+
def set_tokens(self, count: int, *, approximate: bool = False) -> None:
|
|
739
|
+
"""Set the token count.
|
|
740
|
+
|
|
741
|
+
Forces a display refresh even when the value is unchanged. During
|
|
742
|
+
streaming, `show_pending_tokens` replaces the widget text without
|
|
743
|
+
changing the reactive token value, so a later update with the same
|
|
744
|
+
count still needs to re-render the exact count.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
count: Current context token count.
|
|
748
|
+
approximate: Append "+" to indicate the count is stale.
|
|
749
|
+
"""
|
|
750
|
+
self._approximate = approximate
|
|
751
|
+
self._has_token_count = count > 0
|
|
752
|
+
if self.tokens == count:
|
|
753
|
+
# Reactive dedup would skip the watcher — call render directly.
|
|
754
|
+
self._render_tokens(count, approximate=approximate)
|
|
755
|
+
else:
|
|
756
|
+
# Reactive assignment triggers watch_tokens, which reads
|
|
757
|
+
# self._approximate for the suffix.
|
|
758
|
+
self.tokens = count
|
|
759
|
+
|
|
760
|
+
def show_pending_tokens(self) -> None:
|
|
761
|
+
"""Show an unknown token count placeholder during streaming."""
|
|
762
|
+
if not self._has_token_count:
|
|
763
|
+
return
|
|
764
|
+
try:
|
|
765
|
+
self.query_one("#tokens-display", Static).update("... tokens")
|
|
766
|
+
except NoMatches:
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
def set_model(self, *, provider: str, model: str, effort: str = "") -> None:
|
|
770
|
+
"""Update the model display text.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
provider: Model provider name (e.g., `'anthropic'`).
|
|
774
|
+
model: Model name (e.g., `'claude-sonnet-4-5'`).
|
|
775
|
+
effort: Reasoning effort label to display (per-session override or
|
|
776
|
+
provider default), or empty when none applies.
|
|
777
|
+
"""
|
|
778
|
+
label = self.query_one("#model-display", ModelLabel)
|
|
779
|
+
label.provider = provider
|
|
780
|
+
label.model = model
|
|
781
|
+
label.effort = effort
|