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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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)