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,257 @@
1
+ """Generic detail modal for a single pending notification.
2
+
3
+ Used by `NotificationCenterScreen` when the user drills into an entry
4
+ whose payload does not have a dedicated modal (e.g. missing-dependency
5
+ notices). Update-available notifications continue to use
6
+ `UpdateAvailableScreen`, which adds a changelog row on top of the
7
+ action list.
8
+
9
+ Dismisses with the selected `ActionId`, or `None` on Esc.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, ClassVar
15
+
16
+ from textual.binding import Binding, BindingType
17
+ from textual.containers import Vertical
18
+ from textual.content import Content
19
+ from textual.message import Message
20
+ from textual.screen import ModalScreen
21
+ from textual.widgets import Static
22
+
23
+ if TYPE_CHECKING:
24
+ from textual.app import ComposeResult
25
+ from textual.events import Click
26
+
27
+ from deepagents_code.notifications import (
28
+ ActionId,
29
+ NotificationAction,
30
+ PendingNotification,
31
+ )
32
+
33
+ from deepagents_code import theme
34
+ from deepagents_code.config import get_glyphs, is_ascii_mode
35
+
36
+
37
+ class DetailActionActivated(Message):
38
+ """Posted when an `_ActionOption` is clicked with the mouse."""
39
+
40
+ def __init__(self, action_id: ActionId) -> None:
41
+ """Initialize the message.
42
+
43
+ Args:
44
+ action_id: The action the clicked row represents.
45
+ """
46
+ super().__init__()
47
+ self.action_id = action_id
48
+
49
+
50
+ class _ActionOption(Static):
51
+ """Clickable single-line action row."""
52
+
53
+ def __init__(self, action: NotificationAction, widget_id: str) -> None:
54
+ """Initialize the action row widget.
55
+
56
+ Args:
57
+ action: The action this row represents.
58
+ widget_id: DOM id assigned to the widget.
59
+ """
60
+ super().__init__(id=widget_id, classes="nd-action")
61
+ self._action = action
62
+ self._is_selected = False
63
+ self.update(self._render())
64
+
65
+ @property
66
+ def action(self) -> NotificationAction:
67
+ """Underlying action."""
68
+ return self._action
69
+
70
+ def set_selected(self, selected: bool) -> None:
71
+ """Toggle selection styling.
72
+
73
+ Args:
74
+ selected: Whether this row is currently under the cursor.
75
+ """
76
+ if self._is_selected == selected:
77
+ return
78
+ self._is_selected = selected
79
+ self.set_class(selected, "-selected")
80
+ self.update(self._render())
81
+
82
+ def _render(self) -> Content:
83
+ glyphs = get_glyphs()
84
+ cursor = glyphs.cursor if self._is_selected else " "
85
+ text = f"{cursor} {self._action.label}"
86
+ if self._action.primary:
87
+ return Content.styled(text, "bold")
88
+ return Content(text)
89
+
90
+ def on_click(self, event: Click) -> None:
91
+ """Dispatch a click as a `DetailActionActivated` message."""
92
+ event.stop()
93
+ self.post_message(DetailActionActivated(self._action.action_id))
94
+
95
+
96
+ class NotificationDetailScreen(ModalScreen["ActionId | None"]):
97
+ """Modal displaying a single notification's title, body, and actions.
98
+
99
+ Activation returns the chosen `ActionId` via `dismiss()`; Esc
100
+ returns `None` so the caller can keep the underlying notification
101
+ center open.
102
+ """
103
+
104
+ BINDINGS: ClassVar[list[BindingType]] = [
105
+ Binding("escape", "cancel", "Back", show=False),
106
+ Binding("up", "move_up", "Up", show=False, priority=True),
107
+ Binding("k", "move_up", "Up", show=False, priority=True),
108
+ Binding("down", "move_down", "Down", show=False, priority=True),
109
+ Binding("j", "move_down", "Down", show=False, priority=True),
110
+ Binding("tab", "move_down", "Next", show=False, priority=True),
111
+ Binding("shift+tab", "move_up", "Previous", show=False, priority=True),
112
+ Binding("enter", "activate", "Select", show=False, priority=True),
113
+ ]
114
+
115
+ CSS = """
116
+ NotificationDetailScreen {
117
+ align: center middle;
118
+ }
119
+
120
+ NotificationDetailScreen > Vertical {
121
+ width: 68;
122
+ max-width: 90%;
123
+ height: auto;
124
+ max-height: 80%;
125
+ background: $surface;
126
+ border: solid $primary;
127
+ padding: 1 2;
128
+ }
129
+
130
+ NotificationDetailScreen .nd-title {
131
+ text-style: bold;
132
+ color: $primary;
133
+ text-align: center;
134
+ margin-bottom: 1;
135
+ }
136
+
137
+ NotificationDetailScreen .nd-body {
138
+ color: $text-muted;
139
+ margin-bottom: 1;
140
+ }
141
+
142
+ NotificationDetailScreen .nd-action {
143
+ height: auto;
144
+ padding: 0 1;
145
+ color: $text;
146
+ }
147
+
148
+ NotificationDetailScreen .nd-action:hover {
149
+ background: $surface-lighten-1;
150
+ }
151
+
152
+ NotificationDetailScreen .nd-action.-selected {
153
+ background: $surface-lighten-1;
154
+ }
155
+
156
+ NotificationDetailScreen .nd-help {
157
+ height: 1;
158
+ color: $text-muted;
159
+ text-style: italic;
160
+ margin-top: 1;
161
+ text-align: center;
162
+ }
163
+ """
164
+
165
+ def __init__(self, entry: PendingNotification) -> None:
166
+ """Initialize the screen.
167
+
168
+ Args:
169
+ entry: Notification to render. Its actions drive the row
170
+ list; dismissing via an action returns the chosen
171
+ `ActionId` to the caller.
172
+ """
173
+ super().__init__()
174
+ self._entry = entry
175
+ self._options: list[_ActionOption] = []
176
+ self._selected = 0
177
+
178
+ def compose(self) -> ComposeResult:
179
+ """Compose the modal layout.
180
+
181
+ Yields:
182
+ Title, optional body, one `_ActionOption` per action, and
183
+ a help footer.
184
+ """
185
+ glyphs = get_glyphs()
186
+ with Vertical():
187
+ yield Static(self._entry.title, classes="nd-title", markup=False)
188
+ if self._entry.body:
189
+ yield Static(self._entry.body, classes="nd-body", markup=False)
190
+ for idx, action in enumerate(self._entry.actions):
191
+ option = _ActionOption(action, f"nd-row-{idx}")
192
+ self._options.append(option)
193
+ yield option
194
+ help_text = (
195
+ f"{glyphs.arrow_up}/{glyphs.arrow_down} or Tab navigate "
196
+ f"{glyphs.bullet} Enter select "
197
+ f"{glyphs.bullet} Esc back"
198
+ )
199
+ yield Static(help_text, classes="nd-help")
200
+
201
+ def on_mount(self) -> None:
202
+ """Apply ASCII borders and highlight the primary action by default."""
203
+ if is_ascii_mode():
204
+ container = self.query_one(Vertical)
205
+ colors = theme.get_theme_colors(self)
206
+ container.styles.border = ("ascii", colors.primary)
207
+ if not self._options:
208
+ return
209
+ primary_idx = next(
210
+ (i for i, a in enumerate(self._entry.actions) if a.primary),
211
+ 0,
212
+ )
213
+ self._set_selected(primary_idx)
214
+
215
+ def _set_selected(self, new_index: int) -> None:
216
+ """Move the selection cursor to *new_index*.
217
+
218
+ Raises:
219
+ IndexError: When *new_index* is outside `0..len(self._options)`.
220
+ """
221
+ if not self._options:
222
+ return
223
+ if not 0 <= new_index < len(self._options):
224
+ msg = f"selection {new_index} out of range 0..{len(self._options)}"
225
+ raise IndexError(msg)
226
+ if new_index != self._selected:
227
+ self._options[self._selected].set_selected(selected=False)
228
+ self._selected = new_index
229
+ self._options[new_index].set_selected(selected=True)
230
+
231
+ def action_move_up(self) -> None:
232
+ """Move the cursor up one row (wraps at the top)."""
233
+ if not self._options:
234
+ return
235
+ self._set_selected((self._selected - 1) % len(self._options))
236
+
237
+ def action_move_down(self) -> None:
238
+ """Move the cursor down one row (wraps at the bottom)."""
239
+ if not self._options:
240
+ return
241
+ self._set_selected((self._selected + 1) % len(self._options))
242
+
243
+ def action_activate(self) -> None:
244
+ """Dismiss with the highlighted action's id."""
245
+ if not self._options:
246
+ self.dismiss(None)
247
+ return
248
+ self.dismiss(self._options[self._selected].action.action_id)
249
+
250
+ def action_cancel(self) -> None:
251
+ """Close without firing any action."""
252
+ self.dismiss(None)
253
+
254
+ def on_detail_action_activated(self, message: DetailActionActivated) -> None:
255
+ """Handle a mouse click on an action row."""
256
+ message.stop()
257
+ self.dismiss(message.action_id)
@@ -0,0 +1,170 @@
1
+ """Notification settings screen for `/notifications` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, ClassVar
8
+
9
+ from textual.binding import Binding, BindingType
10
+ from textual.containers import VerticalGroup
11
+ from textual.screen import ModalScreen
12
+ from textual.widgets import Checkbox, Static
13
+
14
+ if TYPE_CHECKING:
15
+ from textual.app import ComposeResult
16
+
17
+ from deepagents_code import theme
18
+ from deepagents_code.config import get_glyphs, is_ascii_mode
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Warning keys and their user-facing labels.
23
+ # Checked = warning is shown at startup (not suppressed). Unchecked = suppressed.
24
+ WARNING_TOGGLES: list[tuple[str, str]] = [
25
+ ("ripgrep", "Warn when ripgrep is not installed"),
26
+ ("tavily", "Warn when TAVILY_API_KEY is not set (web search)"),
27
+ ]
28
+
29
+
30
+ class NotificationSettingsScreen(ModalScreen[None]):
31
+ """Modal dialog for managing startup warning preferences.
32
+
33
+ Each checkbox maps to a key in `[warnings].suppress` in
34
+ `~/.deepagents/config.toml`. Toggling a checkbox immediately
35
+ persists the change.
36
+ """
37
+
38
+ BINDINGS: ClassVar[list[BindingType]] = [
39
+ Binding("escape", "cancel", "Close", show=False),
40
+ Binding("up", "app.focus_previous", "Previous", show=False, priority=True),
41
+ Binding("down", "app.focus_next", "Next", show=False, priority=True),
42
+ Binding("tab", "app.focus_next", "Next", show=False, priority=True),
43
+ Binding(
44
+ "shift+tab",
45
+ "app.focus_previous",
46
+ "Previous",
47
+ show=False,
48
+ priority=True,
49
+ ),
50
+ ]
51
+
52
+ CSS = """
53
+ NotificationSettingsScreen {
54
+ align: center middle;
55
+ background: transparent;
56
+ }
57
+
58
+ NotificationSettingsScreen > VerticalGroup {
59
+ width: 65;
60
+ max-width: 90%;
61
+ height: auto;
62
+ max-height: 80%;
63
+ background: $surface;
64
+ border: solid $primary;
65
+ padding: 1 2;
66
+ }
67
+
68
+ NotificationSettingsScreen .ns-title {
69
+ text-style: bold;
70
+ color: $primary;
71
+ text-align: center;
72
+ margin-bottom: 1;
73
+ }
74
+
75
+ NotificationSettingsScreen .ns-help {
76
+ height: 1;
77
+ color: $text-muted;
78
+ text-style: italic;
79
+ margin-top: 1;
80
+ text-align: center;
81
+ }
82
+
83
+ NotificationSettingsScreen Checkbox {
84
+ margin: 0;
85
+ border: none;
86
+ &:focus {
87
+ border: none;
88
+ }
89
+ }
90
+ """
91
+
92
+ def __init__(self, suppressed: set[str]) -> None:
93
+ """Initialize the notification settings screen.
94
+
95
+ Args:
96
+ suppressed: Set of currently suppressed warning keys.
97
+ """
98
+ super().__init__()
99
+ self._suppressed = suppressed
100
+
101
+ def compose(self) -> ComposeResult:
102
+ """Compose the screen layout.
103
+
104
+ Yields:
105
+ Widgets for the notification settings UI.
106
+ """
107
+ glyphs = get_glyphs()
108
+ with VerticalGroup():
109
+ yield Static("Notification Settings", classes="ns-title")
110
+ for key, label in WARNING_TOGGLES:
111
+ yield Checkbox(
112
+ label,
113
+ value=key not in self._suppressed,
114
+ id=f"ns-{key}",
115
+ )
116
+ help_text = (
117
+ f"{glyphs.arrow_up}/{glyphs.arrow_down} or Tab navigate"
118
+ f" {glyphs.bullet} Space toggle"
119
+ f" {glyphs.bullet} Esc close"
120
+ )
121
+ yield Static(help_text, classes="ns-help")
122
+
123
+ def on_mount(self) -> None:
124
+ """Apply ASCII border if needed."""
125
+ if is_ascii_mode():
126
+ container = self.query_one(VerticalGroup)
127
+ colors = theme.get_theme_colors(self)
128
+ container.styles.border = ("ascii", colors.success)
129
+
130
+ def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
131
+ """Persist warning suppression toggle to config.toml on change."""
132
+ event.stop()
133
+ checkbox_id = event.checkbox.id
134
+ if not checkbox_id or not checkbox_id.startswith("ns-"):
135
+ return
136
+ key = checkbox_id.removeprefix("ns-")
137
+ enabled = event.value
138
+
139
+ async def _persist() -> None:
140
+ from deepagents_code.model_config import (
141
+ suppress_warning,
142
+ unsuppress_warning,
143
+ )
144
+
145
+ try:
146
+ if enabled:
147
+ ok = await asyncio.to_thread(unsuppress_warning, key)
148
+ else:
149
+ ok = await asyncio.to_thread(suppress_warning, key)
150
+ except Exception:
151
+ logger.warning(
152
+ "Failed to persist notification setting for %r",
153
+ key,
154
+ exc_info=True,
155
+ )
156
+ ok = False
157
+ if not ok:
158
+ self.app.notify(
159
+ "Could not save notification preference. "
160
+ "Check file permissions for ~/.deepagents/config.toml.",
161
+ severity="warning",
162
+ timeout=6,
163
+ markup=False,
164
+ )
165
+
166
+ self.call_later(_persist)
167
+
168
+ def action_cancel(self) -> None:
169
+ """Close the screen."""
170
+ self.dismiss(None)
@@ -0,0 +1,158 @@
1
+ """Confirmation modal offered when a change needs an owned-server respawn.
2
+
3
+ Some changes take effect only when the app-owned LangGraph server subprocess
4
+ spawns: provider/sandbox extras and `--package` installs are imported at spawn
5
+ time, and a Tavily key saved via `/auth` binds the `web_search` tool only at
6
+ spawn time. A `/restart` respawns the subprocess without exiting the TUI, so
7
+ rather than make the user type `/restart` by hand, this modal offers to run
8
+ that restart immediately while leaving deferral one keypress away. The title
9
+ `verb` and `body` are caller-supplied so one modal serves each flow.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, ClassVar, Literal
15
+
16
+ from textual.binding import Binding, BindingType
17
+ from textual.containers import Vertical
18
+ from textual.content import Content
19
+ from textual.screen import ModalScreen
20
+ from textual.widgets import Static
21
+
22
+ from deepagents_code.config import get_glyphs
23
+
24
+ if TYPE_CHECKING:
25
+ from textual.app import ComposeResult
26
+
27
+
28
+ RestartChoice = Literal["restart", "later"]
29
+ """Outcome of the prompt: restart the server now or defer."""
30
+
31
+
32
+ class RestartPromptScreen(ModalScreen[RestartChoice]):
33
+ """Modal asking whether to restart the server for a spawn-time change.
34
+
35
+ Serves both the post-install offer and the post-`/auth` web-search offer;
36
+ the caller supplies the title `verb` and `body` copy.
37
+
38
+ Dismisses with `"restart"` when the user accepts and `"later"` when the
39
+ user defers. Esc is treated as "later" so the user is never forced into a
40
+ restart they did not explicitly choose.
41
+ """
42
+
43
+ BINDINGS: ClassVar[list[BindingType]] = [
44
+ Binding("enter", "restart", "Restart", show=False, priority=True),
45
+ Binding("escape", "later", "Later", show=False, priority=True),
46
+ ]
47
+
48
+ CSS = """
49
+ RestartPromptScreen {
50
+ align: center middle;
51
+ }
52
+
53
+ RestartPromptScreen > Vertical {
54
+ width: 64;
55
+ max-width: 90%;
56
+ height: auto;
57
+ background: $surface;
58
+ border: solid $primary;
59
+ padding: 1 2;
60
+ }
61
+
62
+ RestartPromptScreen .restart-prompt-title {
63
+ text-style: bold;
64
+ color: $primary;
65
+ text-align: center;
66
+ margin-bottom: 1;
67
+ }
68
+
69
+ RestartPromptScreen .restart-prompt-body {
70
+ height: auto;
71
+ color: $text;
72
+ margin-bottom: 1;
73
+ }
74
+
75
+ RestartPromptScreen .restart-prompt-help {
76
+ height: 1;
77
+ color: $text-muted;
78
+ text-style: italic;
79
+ text-align: center;
80
+ }
81
+ """
82
+
83
+ _DEFAULT_BODY = "Restart the server to load it now, or defer with `/restart`."
84
+
85
+ def __init__(
86
+ self,
87
+ label: str,
88
+ *,
89
+ verb: str,
90
+ body: str | None = None,
91
+ ) -> None:
92
+ """Initialize the prompt.
93
+
94
+ Args:
95
+ label: The subject surfaced in the title (e.g. an installed extra
96
+ name, or a saved credential like ``"Tavily API key"``).
97
+ verb: Past-tense action shown before `label` in the title — e.g.
98
+ `"Installed"` for the post-install flow or `"Saved"` for a
99
+ saved credential. Required (no default) so each flow states its
100
+ own intent and a future caller can't inherit install-only copy.
101
+ body: Optional override for the explanatory line under the title.
102
+ Defaults to the generic restart copy.
103
+ """
104
+ super().__init__()
105
+ self._label = label
106
+ self._verb = verb
107
+ self._body = body or self._DEFAULT_BODY
108
+
109
+ def compose(self) -> ComposeResult:
110
+ """Compose the confirmation dialog.
111
+
112
+ Yields:
113
+ Title, body, and help-row widgets parented inside a `Vertical`.
114
+ """
115
+ glyphs = get_glyphs()
116
+ with Vertical():
117
+ yield Static(
118
+ Content.from_markup(
119
+ "$check $verb [bold]$name[/bold]",
120
+ check=glyphs.checkmark,
121
+ verb=self._verb,
122
+ name=self._label,
123
+ ),
124
+ classes="restart-prompt-title",
125
+ markup=False,
126
+ )
127
+ yield Static(
128
+ self._body,
129
+ classes="restart-prompt-body",
130
+ markup=False,
131
+ )
132
+ yield Static(
133
+ "Enter to restart, Esc to defer",
134
+ classes="restart-prompt-help",
135
+ markup=False,
136
+ )
137
+
138
+ def action_restart(self) -> None:
139
+ """Dismiss with `"restart"`."""
140
+ self.dismiss("restart")
141
+
142
+ def action_later(self) -> None:
143
+ """Dismiss with `"later"`."""
144
+ self.dismiss("later")
145
+
146
+ def action_cancel(self) -> None:
147
+ """Alias for `action_later` so Esc resolves to a deliberate defer.
148
+
149
+ The app's `action_interrupt` (`escape` binding, `priority=True`)
150
+ fires before this screen's own `escape` binding. When the active
151
+ screen is a `ModalScreen`, it dispatches to `action_cancel` if
152
+ present, else falls through to `dismiss(None)`. The current caller
153
+ no-ops on both `None` and `"later"`, but defining this alias pins Esc
154
+ to an explicit `"later"` — matching the sibling reconnect/cwd-switch
155
+ modals and keeping the outcome unambiguous for any future caller that
156
+ branches on a deliberate defer versus a programmatic dismiss.
157
+ """
158
+ self.action_later()