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,456 @@
|
|
|
1
|
+
"""Notification center modal for pending actionable notices.
|
|
2
|
+
|
|
3
|
+
Surfaces a list of `PendingNotification` entries as single-line rows.
|
|
4
|
+
Selecting a row drills into a dedicated detail modal
|
|
5
|
+
(`UpdateAvailableScreen` for update entries, `NotificationDetailScreen`
|
|
6
|
+
otherwise) stacked on top of the center. When the detail modal
|
|
7
|
+
dismisses with a terminal action (one that closes the center) the
|
|
8
|
+
center dismisses with a `NotificationActionResult` so the app layer can
|
|
9
|
+
dispatch. Actions that must keep the center open are handled in place:
|
|
10
|
+
SUPPRESS via
|
|
11
|
+
`NotificationSuppressRequested` (so the remaining notifications stay
|
|
12
|
+
reachable) and actions in `IN_PLACE_ACTIONS` via
|
|
13
|
+
`NotificationActionRequested` (so a follow-up modal, e.g. the API-key
|
|
14
|
+
prompt, stacks on top and Esc returns to the center). When the detail
|
|
15
|
+
cancels, the center stays open on the list.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
23
|
+
|
|
24
|
+
from textual.binding import Binding, BindingType
|
|
25
|
+
from textual.containers import Vertical, VerticalScroll
|
|
26
|
+
from textual.content import Content
|
|
27
|
+
from textual.message import Message
|
|
28
|
+
from textual.screen import ModalScreen
|
|
29
|
+
from textual.widgets import Static
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from textual.app import ComposeResult
|
|
33
|
+
from textual.events import Click
|
|
34
|
+
|
|
35
|
+
from deepagents_code.notifications import PendingNotification
|
|
36
|
+
|
|
37
|
+
from deepagents_code import theme
|
|
38
|
+
from deepagents_code.config import get_glyphs, is_ascii_mode
|
|
39
|
+
from deepagents_code.notifications import ActionId, UpdateAvailablePayload
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class NotificationActionResult:
|
|
46
|
+
"""Dismissal payload identifying which action the user picked.
|
|
47
|
+
|
|
48
|
+
The screen returns this via `dismiss()` when the user drills into
|
|
49
|
+
a notification and selects an action; it returns `None` when the
|
|
50
|
+
user cancels with Esc without committing to an action.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
key: str
|
|
54
|
+
"""Registry key of the notification the action was picked for."""
|
|
55
|
+
|
|
56
|
+
action_id: ActionId
|
|
57
|
+
"""Identifier of the chosen `NotificationAction`."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NotificationRowClicked(Message):
|
|
61
|
+
"""Posted when a notification row is clicked with the mouse."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, key: str) -> None:
|
|
64
|
+
"""Initialize the message.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
key: Registry key of the clicked notification. Using a key
|
|
68
|
+
instead of an index keeps the message valid across
|
|
69
|
+
`reload()` rebuilds, which replace the row list.
|
|
70
|
+
"""
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.key = key
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NotificationSuppressRequested(Message):
|
|
76
|
+
"""Posted when the user picks SUPPRESS from a notification's detail modal.
|
|
77
|
+
|
|
78
|
+
The center does not dismiss on SUPPRESS because the remaining
|
|
79
|
+
notifications should still be reachable in place. The app handles
|
|
80
|
+
this message by running the suppress dispatch and calling
|
|
81
|
+
`NotificationCenterScreen.reload` with the refreshed registry
|
|
82
|
+
snapshot.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, key: str) -> None:
|
|
86
|
+
"""Initialize the message.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
key: Registry key of the notification being suppressed.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__()
|
|
92
|
+
self.key = key
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
IN_PLACE_ACTIONS: frozenset[ActionId] = frozenset({ActionId.ENTER_API_KEY})
|
|
96
|
+
"""Actions handled in place without dismissing the center.
|
|
97
|
+
|
|
98
|
+
Each opens a follow-up modal on top of the center, so the center stays
|
|
99
|
+
mounted and Esc in that modal returns here (rationale in
|
|
100
|
+
`NotificationActionRequested`). SUPPRESS is also handled in place but
|
|
101
|
+
routes through its own `NotificationSuppressRequested` message, so it is
|
|
102
|
+
deliberately excluded from this set.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NotificationActionRequested(Message):
|
|
107
|
+
"""Posted for an action that opens a follow-up modal in place.
|
|
108
|
+
|
|
109
|
+
Some actions (those in `IN_PLACE_ACTIONS`, currently `ENTER_API_KEY`)
|
|
110
|
+
push another modal, such as the API-key prompt, on top of the
|
|
111
|
+
still-open center. Dismissing the center first would drop that stack,
|
|
112
|
+
so Esc in the follow-up modal would fall through to the base screen
|
|
113
|
+
instead of returning here. The app handles this message by dispatching
|
|
114
|
+
the action while the center stays mounted, then reloading it with the
|
|
115
|
+
refreshed registry snapshot.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, key: str, action_id: ActionId) -> None:
|
|
119
|
+
"""Initialize the message.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
key: Registry key of the notification the action targets.
|
|
123
|
+
action_id: The in-place action the user selected. Must be a
|
|
124
|
+
member of `IN_PLACE_ACTIONS`.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If `action_id` is not an in-place action, which
|
|
128
|
+
would be a programmer error (the message is only meant to
|
|
129
|
+
carry actions that keep the center open).
|
|
130
|
+
"""
|
|
131
|
+
super().__init__()
|
|
132
|
+
if action_id not in IN_PLACE_ACTIONS:
|
|
133
|
+
msg = f"{action_id} is not an in-place action"
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
self.key = key
|
|
136
|
+
self.action_id = action_id
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _NotificationRow(Static):
|
|
140
|
+
"""Clickable single-line row displaying a notification's title."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, notification: PendingNotification, index: int) -> None:
|
|
143
|
+
"""Initialize the row widget.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
notification: The entry to render.
|
|
147
|
+
index: Position in the parent's list.
|
|
148
|
+
"""
|
|
149
|
+
super().__init__(id=f"nc-row-{index}", classes="nc-row")
|
|
150
|
+
self._notification = notification
|
|
151
|
+
self._index = index
|
|
152
|
+
self._is_selected = False
|
|
153
|
+
self.update(self._render())
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def notification(self) -> PendingNotification:
|
|
157
|
+
"""Underlying notification."""
|
|
158
|
+
return self._notification
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def index(self) -> int:
|
|
162
|
+
"""Row index in the parent list."""
|
|
163
|
+
return self._index
|
|
164
|
+
|
|
165
|
+
def set_selected(self, selected: bool) -> None:
|
|
166
|
+
"""Toggle selection styling.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
selected: Whether this row is currently under the cursor.
|
|
170
|
+
"""
|
|
171
|
+
if self._is_selected == selected:
|
|
172
|
+
return
|
|
173
|
+
self._is_selected = selected
|
|
174
|
+
self.set_class(selected, "-selected")
|
|
175
|
+
self.update(self._render())
|
|
176
|
+
|
|
177
|
+
def _render(self) -> Content:
|
|
178
|
+
glyphs = get_glyphs()
|
|
179
|
+
cursor = glyphs.cursor if self._is_selected else " "
|
|
180
|
+
return Content.assemble(
|
|
181
|
+
f"{cursor} ",
|
|
182
|
+
(self._notification.title, "bold"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def on_click(self, event: Click) -> None:
|
|
186
|
+
"""Dispatch a click as a `NotificationRowClicked` message."""
|
|
187
|
+
event.stop()
|
|
188
|
+
self.post_message(NotificationRowClicked(self._notification.key))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class NotificationCenterScreen(ModalScreen[NotificationActionResult | None]):
|
|
192
|
+
"""Modal listing pending notifications with drill-in details.
|
|
193
|
+
|
|
194
|
+
Each `PendingNotification` is a single row. Up/Down (or j/k)
|
|
195
|
+
moves the cursor; Enter or click pushes a detail modal for the
|
|
196
|
+
highlighted entry. The detail modal carries the action list and
|
|
197
|
+
dismisses with an `ActionId` or `None`. Esc on the center returns
|
|
198
|
+
`None`.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
202
|
+
Binding("escape", "cancel", "Close", show=False),
|
|
203
|
+
Binding("up", "move_up", "Up", show=False, priority=True),
|
|
204
|
+
Binding("k", "move_up", "Up", show=False, priority=True),
|
|
205
|
+
Binding("down", "move_down", "Down", show=False, priority=True),
|
|
206
|
+
Binding("j", "move_down", "Down", show=False, priority=True),
|
|
207
|
+
Binding("tab", "move_down", "Next", show=False, priority=True),
|
|
208
|
+
Binding("enter", "activate", "Open", show=False, priority=True),
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
CSS = """
|
|
212
|
+
NotificationCenterScreen {
|
|
213
|
+
align: center middle;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
NotificationCenterScreen > Vertical {
|
|
217
|
+
width: 72;
|
|
218
|
+
max-width: 90%;
|
|
219
|
+
height: auto;
|
|
220
|
+
max-height: 80%;
|
|
221
|
+
background: $surface;
|
|
222
|
+
border: solid $primary;
|
|
223
|
+
padding: 1 2;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
NotificationCenterScreen .nc-title {
|
|
227
|
+
text-style: bold;
|
|
228
|
+
color: $primary;
|
|
229
|
+
text-align: center;
|
|
230
|
+
margin-bottom: 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
NotificationCenterScreen VerticalScroll {
|
|
234
|
+
height: auto;
|
|
235
|
+
max-height: 24;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
NotificationCenterScreen .nc-row {
|
|
239
|
+
height: 1;
|
|
240
|
+
padding: 0 1;
|
|
241
|
+
color: $text;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
NotificationCenterScreen .nc-row:hover {
|
|
245
|
+
background: $surface-lighten-1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
NotificationCenterScreen .nc-row.-selected {
|
|
249
|
+
background: $surface-lighten-1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
NotificationCenterScreen .nc-help {
|
|
253
|
+
height: 1;
|
|
254
|
+
color: $text-muted;
|
|
255
|
+
text-style: italic;
|
|
256
|
+
margin-top: 1;
|
|
257
|
+
text-align: center;
|
|
258
|
+
}
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, notifications: list[PendingNotification]) -> None:
|
|
262
|
+
"""Initialize the screen with a snapshot of pending notifications.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
notifications: Entries to render. Order is preserved.
|
|
266
|
+
"""
|
|
267
|
+
super().__init__()
|
|
268
|
+
self._notifications = notifications
|
|
269
|
+
self._selected: int = 0
|
|
270
|
+
self._rows: list[_NotificationRow] = []
|
|
271
|
+
self._drilling = False
|
|
272
|
+
|
|
273
|
+
def compose(self) -> ComposeResult:
|
|
274
|
+
"""Compose the modal layout.
|
|
275
|
+
|
|
276
|
+
Yields:
|
|
277
|
+
The title widget, one row per pending notification, and a
|
|
278
|
+
help footer.
|
|
279
|
+
"""
|
|
280
|
+
glyphs = get_glyphs()
|
|
281
|
+
with Vertical():
|
|
282
|
+
yield Static("Notifications", classes="nc-title")
|
|
283
|
+
with VerticalScroll():
|
|
284
|
+
for idx, notif in enumerate(self._notifications):
|
|
285
|
+
row = _NotificationRow(notif, idx)
|
|
286
|
+
self._rows.append(row)
|
|
287
|
+
yield row
|
|
288
|
+
help_text = (
|
|
289
|
+
f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate "
|
|
290
|
+
f"{glyphs.bullet} Enter open "
|
|
291
|
+
f"{glyphs.bullet} Esc close"
|
|
292
|
+
)
|
|
293
|
+
yield Static(help_text, classes="nc-help")
|
|
294
|
+
|
|
295
|
+
def on_mount(self) -> None:
|
|
296
|
+
"""Apply ASCII borders and highlight the first row."""
|
|
297
|
+
if is_ascii_mode():
|
|
298
|
+
container = self.query_one(Vertical)
|
|
299
|
+
colors = theme.get_theme_colors(self)
|
|
300
|
+
container.styles.border = ("ascii", colors.primary)
|
|
301
|
+
if self._rows:
|
|
302
|
+
self._rows[0].set_selected(selected=True)
|
|
303
|
+
self._rows[0].scroll_visible()
|
|
304
|
+
|
|
305
|
+
def _set_selected(self, new_index: int) -> None:
|
|
306
|
+
"""Move the selection cursor to *new_index*.
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
IndexError: If *new_index* is outside `0..len(self._rows)`.
|
|
310
|
+
"""
|
|
311
|
+
if not self._rows or new_index == self._selected:
|
|
312
|
+
return
|
|
313
|
+
if not 0 <= new_index < len(self._rows):
|
|
314
|
+
msg = f"selection {new_index} out of range 0..{len(self._rows)}"
|
|
315
|
+
raise IndexError(msg)
|
|
316
|
+
self._rows[self._selected].set_selected(selected=False)
|
|
317
|
+
self._selected = new_index
|
|
318
|
+
self._rows[new_index].set_selected(selected=True)
|
|
319
|
+
self._rows[new_index].scroll_visible()
|
|
320
|
+
|
|
321
|
+
def action_move_up(self) -> None:
|
|
322
|
+
"""Move the cursor up one row (wraps at the top)."""
|
|
323
|
+
if not self._rows:
|
|
324
|
+
return
|
|
325
|
+
self._set_selected((self._selected - 1) % len(self._rows))
|
|
326
|
+
|
|
327
|
+
def action_move_down(self) -> None:
|
|
328
|
+
"""Move the cursor down one row (wraps at the bottom)."""
|
|
329
|
+
if not self._rows:
|
|
330
|
+
return
|
|
331
|
+
self._set_selected((self._selected + 1) % len(self._rows))
|
|
332
|
+
|
|
333
|
+
def action_activate(self) -> None:
|
|
334
|
+
"""Drill into the highlighted notification."""
|
|
335
|
+
if not self._rows:
|
|
336
|
+
self.dismiss(None)
|
|
337
|
+
return
|
|
338
|
+
self._drill_into(self._notifications[self._selected])
|
|
339
|
+
|
|
340
|
+
def action_cancel(self) -> None:
|
|
341
|
+
"""Close without firing any action."""
|
|
342
|
+
self.dismiss(None)
|
|
343
|
+
|
|
344
|
+
def on_notification_row_clicked(self, message: NotificationRowClicked) -> None:
|
|
345
|
+
"""Handle a mouse click on a notification row."""
|
|
346
|
+
message.stop()
|
|
347
|
+
index = next(
|
|
348
|
+
(i for i, n in enumerate(self._notifications) if n.key == message.key),
|
|
349
|
+
None,
|
|
350
|
+
)
|
|
351
|
+
if index is None:
|
|
352
|
+
# Row was rebuilt out from under the click (reload race);
|
|
353
|
+
# surface at debug level so regressions stay diagnosable.
|
|
354
|
+
logger.debug("Ignoring click on unknown notification key %r", message.key)
|
|
355
|
+
return
|
|
356
|
+
self._set_selected(index)
|
|
357
|
+
self._drill_into(self._notifications[index])
|
|
358
|
+
|
|
359
|
+
def _drill_into(self, entry: PendingNotification) -> None:
|
|
360
|
+
"""Push a detail modal for *entry*.
|
|
361
|
+
|
|
362
|
+
Guarded against reentry so a rapid double-activation (e.g.
|
|
363
|
+
keyboard repeat) does not stack two detail modals.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
entry: The notification to drill into.
|
|
367
|
+
"""
|
|
368
|
+
if self._drilling:
|
|
369
|
+
return
|
|
370
|
+
detail_screen = self._detail_screen_for(entry)
|
|
371
|
+
self._drilling = True
|
|
372
|
+
|
|
373
|
+
def handle_detail(action_id: ActionId | None) -> None:
|
|
374
|
+
self._drilling = False
|
|
375
|
+
if action_id is None:
|
|
376
|
+
return
|
|
377
|
+
if action_id == ActionId.SUPPRESS:
|
|
378
|
+
# Keep the center open; the app handles dispatch and
|
|
379
|
+
# calls `reload` with the refreshed list. Rationale is
|
|
380
|
+
# in `NotificationSuppressRequested`'s class docstring.
|
|
381
|
+
self.post_message(NotificationSuppressRequested(entry.key))
|
|
382
|
+
return
|
|
383
|
+
if action_id in IN_PLACE_ACTIONS:
|
|
384
|
+
# Keep the center open so the follow-up modal (e.g. the
|
|
385
|
+
# API-key prompt) stacks on top and Esc returns here.
|
|
386
|
+
# Rationale is in `NotificationActionRequested`'s docstring.
|
|
387
|
+
self.post_message(NotificationActionRequested(entry.key, action_id))
|
|
388
|
+
return
|
|
389
|
+
self.dismiss(NotificationActionResult(entry.key, action_id))
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
self.app.push_screen(detail_screen, handle_detail)
|
|
393
|
+
except Exception:
|
|
394
|
+
# push_screen raising would otherwise leave `_drilling`
|
|
395
|
+
# permanently True and wedge the center.
|
|
396
|
+
self._drilling = False
|
|
397
|
+
raise
|
|
398
|
+
|
|
399
|
+
async def reload(self, notifications: list[PendingNotification]) -> None:
|
|
400
|
+
"""Rebuild the row list from a refreshed snapshot.
|
|
401
|
+
|
|
402
|
+
Preserves cursor position by key when possible; falls back to
|
|
403
|
+
clamping the previous index into the new bounds. Dismisses the
|
|
404
|
+
screen with `None` when the list is empty.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
notifications: Current pending entries to display.
|
|
408
|
+
"""
|
|
409
|
+
if not notifications:
|
|
410
|
+
self.dismiss(None)
|
|
411
|
+
return
|
|
412
|
+
prev_key: str | None = None
|
|
413
|
+
if self._notifications and 0 <= self._selected < len(self._notifications):
|
|
414
|
+
prev_key = self._notifications[self._selected].key
|
|
415
|
+
new_selected = next(
|
|
416
|
+
(i for i, n in enumerate(notifications) if n.key == prev_key),
|
|
417
|
+
min(self._selected, len(notifications) - 1) if prev_key else 0,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
scroll = self.query_one(VerticalScroll)
|
|
421
|
+
await scroll.remove_children()
|
|
422
|
+
new_rows = [
|
|
423
|
+
_NotificationRow(notif, idx) for idx, notif in enumerate(notifications)
|
|
424
|
+
]
|
|
425
|
+
await scroll.mount(*new_rows)
|
|
426
|
+
self._rows = new_rows
|
|
427
|
+
self._notifications = notifications
|
|
428
|
+
self._selected = new_selected
|
|
429
|
+
new_rows[new_selected].set_selected(selected=True)
|
|
430
|
+
new_rows[new_selected].scroll_visible()
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _detail_screen_for(
|
|
434
|
+
entry: PendingNotification,
|
|
435
|
+
) -> ModalScreen[ActionId | None]:
|
|
436
|
+
"""Pick the appropriate detail modal for *entry*'s payload.
|
|
437
|
+
|
|
438
|
+
Update-available entries use the dedicated
|
|
439
|
+
`UpdateAvailableScreen` (which adds a changelog row); all
|
|
440
|
+
other payloads use the generic `NotificationDetailScreen`.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
A `ModalScreen` whose `dismiss()` payload is the selected
|
|
444
|
+
`ActionId` or `None` when the user cancels.
|
|
445
|
+
"""
|
|
446
|
+
if isinstance(entry.payload, UpdateAvailablePayload):
|
|
447
|
+
from deepagents_code.tui.widgets.update_available import (
|
|
448
|
+
UpdateAvailableScreen,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return UpdateAvailableScreen(entry)
|
|
452
|
+
from deepagents_code.tui.widgets.notification_detail import (
|
|
453
|
+
NotificationDetailScreen,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return NotificationDetailScreen(entry)
|