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,539 @@
1
+ """In-TUI MCP OAuth login modal.
2
+
3
+ `MCPLoginScreen` is both a Textual `ModalScreen` and an implementation of
4
+ `OAuthInteraction`. The login worker awaits its interaction methods while
5
+ the user sees and acts on the modal's widgets — authorize URLs become
6
+ clickable links, paste-back callback URLs go through an inline input row,
7
+ device-code instructions render inline, and the modal closes itself on
8
+ success.
9
+
10
+ The screen runs on the Textual event loop (same loop as the worker), so
11
+ methods called from the worker can `await` modal-bound futures directly
12
+ without `app.call_from_thread`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from typing import TYPE_CHECKING, ClassVar, Literal
19
+
20
+ from textual.binding import Binding, BindingType
21
+ from textual.containers import Vertical, VerticalScroll
22
+ from textual.content import Content
23
+ from textual.events import (
24
+ Click, # noqa: TC002 - needed at runtime for Textual event dispatch
25
+ MouseMove, # noqa: TC002 - needed at runtime for Textual event dispatch
26
+ )
27
+ from textual.screen import ModalScreen
28
+ from textual.style import Style as TStyle
29
+ from textual.widgets import Input, Static
30
+
31
+ from deepagents_code import theme
32
+ from deepagents_code.config import get_glyphs, is_ascii_mode
33
+ from deepagents_code.tui.widgets._links import open_style_link
34
+ from deepagents_code.tui.widgets.loading import Spinner
35
+
36
+ if TYPE_CHECKING:
37
+ from textual.app import ComposeResult
38
+ from textual.timer import Timer
39
+
40
+
41
+ LoginOutcome = Literal["success", "cancelled", "failed"]
42
+ """Discriminator returned by the modal when it dismisses."""
43
+
44
+
45
+ class MCPLoginCancelledError(RuntimeError):
46
+ """Raised by `MCPLoginScreen.action_cancel` when the user cancels the flow."""
47
+
48
+
49
+ _PROMPT_CALLBACK = "Paste the full callback URL after approving in the browser:"
50
+
51
+
52
+ class MCPLoginScreen(ModalScreen[LoginOutcome]):
53
+ """Modal that renders the OAuth login flow and collects user input.
54
+
55
+ Implements the `OAuthInteraction` Protocol structurally so a
56
+ `mcp_auth.login(..., ui=screen)` call drives the same modal. Each
57
+ interaction method updates a status line, a clickable link area, and
58
+ an inline input prompt for the callback URL. Slack workspace selection
59
+ is deferred to Slack's browser page rather than prompted inline.
60
+ """
61
+
62
+ BINDINGS: ClassVar[list[BindingType]] = [
63
+ Binding("enter", "toggle_authorize_url", "Toggle URL", show=False),
64
+ Binding("escape", "cancel", "Cancel", show=False, priority=True),
65
+ ]
66
+ """Esc unblocks the worker; the worker performs the actual shutdown.
67
+
68
+ Cancellation completes any outstanding input future with
69
+ `MCPLoginCancelledError`. The worker (`_run_mcp_login_worker`) sees
70
+ that exception, calls `finish(success=False)`, and tears down the
71
+ OAuth handshake. Doing the teardown here would race the worker.
72
+ """
73
+
74
+ CSS = """
75
+ MCPLoginScreen {
76
+ align: center middle;
77
+ }
78
+
79
+ MCPLoginScreen > Vertical {
80
+ width: 80;
81
+ max-width: 92%;
82
+ height: auto;
83
+ max-height: 85%;
84
+ background: $surface;
85
+ border: solid $primary;
86
+ padding: 1 2;
87
+ }
88
+
89
+ MCPLoginScreen .ml-title {
90
+ text-style: bold;
91
+ color: $primary;
92
+ text-align: center;
93
+ margin-bottom: 1;
94
+ }
95
+
96
+ MCPLoginScreen .ml-status {
97
+ height: auto;
98
+ color: $text;
99
+ margin-bottom: 1;
100
+ }
101
+
102
+ MCPLoginScreen .ml-link {
103
+ height: auto;
104
+ color: $accent;
105
+ margin-bottom: 1;
106
+ }
107
+
108
+ MCPLoginScreen .ml-history {
109
+ height: auto;
110
+ max-height: 8;
111
+ background: $surface-lighten-1;
112
+ margin-bottom: 1;
113
+ }
114
+
115
+ MCPLoginScreen .ml-history-line {
116
+ height: auto;
117
+ color: $text-muted;
118
+ padding: 0 1;
119
+ }
120
+
121
+ MCPLoginScreen .ml-prompt {
122
+ height: 1;
123
+ color: $text;
124
+ }
125
+
126
+ MCPLoginScreen #ml-input {
127
+ margin-bottom: 1;
128
+ border: solid $primary-lighten-2;
129
+ }
130
+
131
+ MCPLoginScreen #ml-input:focus {
132
+ border: solid $primary;
133
+ }
134
+
135
+ MCPLoginScreen .ml-help {
136
+ height: 1;
137
+ color: $text-muted;
138
+ text-style: italic;
139
+ text-align: center;
140
+ }
141
+ """
142
+
143
+ def __init__(self, server_name: str) -> None:
144
+ """Initialize a login modal for `server_name`.
145
+
146
+ Args:
147
+ server_name: MCP server name shown in the modal title.
148
+ """
149
+ super().__init__()
150
+ self._server_name = server_name
151
+ self._status = f"Starting OAuth login for {server_name}…"
152
+ self._title_widget: Static | None = None
153
+ self._status_widget: Static | None = None
154
+ self._link_widget: Static | None = None
155
+ self._history_widget: VerticalScroll | None = None
156
+ self._prompt_widget: Static | None = None
157
+ self._input_widget: Input | None = None
158
+ self._help_widget: Static | None = None
159
+ self._spinner = Spinner()
160
+ self._spinner_timer: Timer | None = None
161
+ self._authorize_url: str | None = None
162
+ self._authorize_url_opened_in_browser = False
163
+ self._authorize_url_expanded = False
164
+ self._waiting_for_authorization = False
165
+
166
+ # Each prompt method blocks on a fresh Future; cancellation completes
167
+ # it with MCPLoginCancelledError so the worker unblocks rather than
168
+ # hanging on a dismissed modal.
169
+ self._pending_input: asyncio.Future[str] | None = None
170
+ self._cancelled = False
171
+ self._done = False
172
+ self._outcome: LoginOutcome | None = None
173
+ self._last_history_line: str | None = None
174
+
175
+ @property
176
+ def is_done(self) -> bool:
177
+ """`True` once the modal has been told to finish (success or failure).
178
+
179
+ Public accessor for callers that need to coordinate teardown from
180
+ outside the screen, e.g. the worker's `BaseException` branch that
181
+ unblocks dismiss without re-finishing.
182
+ """
183
+ return self._done
184
+
185
+ def compose(self) -> ComposeResult:
186
+ """Compose the modal body inside a `Vertical` container.
187
+
188
+ Yields:
189
+ Title, status, optional link, history, prompt label, input,
190
+ and help footer widgets, all parented inside a `Vertical`.
191
+ """
192
+ with Vertical():
193
+ self._title_widget = Static(
194
+ Content.from_markup("MCP login: $name", name=self._server_name),
195
+ classes="ml-title",
196
+ markup=False,
197
+ )
198
+ yield self._title_widget
199
+ self._status_widget = Static(
200
+ self._status, classes="ml-status", markup=False
201
+ )
202
+ yield self._status_widget
203
+ self._link_widget = Static("", classes="ml-link")
204
+ self._link_widget.display = False
205
+ yield self._link_widget
206
+ self._history_widget = VerticalScroll(classes="ml-history")
207
+ self._history_widget.display = False
208
+ yield self._history_widget
209
+ self._prompt_widget = Static("", classes="ml-prompt", markup=False)
210
+ self._prompt_widget.display = False
211
+ yield self._prompt_widget
212
+ self._input_widget = Input(id="ml-input")
213
+ self._input_widget.display = False
214
+ yield self._input_widget
215
+ self._help_widget = Static("Esc to cancel", classes="ml-help")
216
+ yield self._help_widget
217
+
218
+ def on_mount(self) -> None:
219
+ """Apply ASCII border when configured and start the spinner ticker."""
220
+ if is_ascii_mode():
221
+ container = self.query_one(Vertical)
222
+ colors = theme.get_theme_colors(self)
223
+ container.styles.border = ("ascii", colors.primary)
224
+ self._spinner_timer = self.set_interval(0.1, self._tick_spinner)
225
+
226
+ def on_click(self, event: Click) -> None:
227
+ """Open style links, or expand/collapse the manual authorize URL."""
228
+ if (
229
+ event.widget is self._link_widget
230
+ and self._authorize_url is not None
231
+ and self._authorize_url_opened_in_browser
232
+ and not event.style.link
233
+ ):
234
+ self._toggle_authorize_url()
235
+ event.stop()
236
+ return
237
+ open_style_link(event)
238
+
239
+ def on_mouse_move(self, event: MouseMove) -> None:
240
+ """Show a pointer over links and the manual URL disclosure row."""
241
+ self.styles.pointer = (
242
+ "pointer"
243
+ if event.style.link or event.widget is self._link_widget
244
+ else "default"
245
+ )
246
+
247
+ def on_leave(self) -> None:
248
+ """Reset the pointer shape when the mouse leaves the modal."""
249
+ self.styles.pointer = "default"
250
+
251
+ # ------------------------------------------------------------------
252
+ # OAuthInteraction implementation.
253
+ # ------------------------------------------------------------------
254
+
255
+ async def show_authorize_url(self, url: str, *, opened_in_browser: bool) -> None:
256
+ """Render browser-open status and only reveal the URL when needed."""
257
+ self._authorize_url = url
258
+ self._authorize_url_opened_in_browser = opened_in_browser
259
+ self._authorize_url_expanded = not opened_in_browser
260
+ self._waiting_for_authorization = opened_in_browser
261
+ if opened_in_browser:
262
+ self._render_authorization_wait_status(self._spinner.next_frame())
263
+ else:
264
+ self._set_status(
265
+ f"Open the authorization URL manually to connect {self._server_name}.",
266
+ )
267
+ self._render_authorize_url()
268
+
269
+ async def request_callback_url(self) -> str:
270
+ """Wait for the user to paste back the OAuth callback URL.
271
+
272
+ Returns:
273
+ The trimmed callback URL.
274
+ """
275
+ return await self._await_input(_PROMPT_CALLBACK)
276
+
277
+ async def show_device_code(
278
+ self,
279
+ *,
280
+ verification_uri: str,
281
+ user_code: str,
282
+ expires_in: int,
283
+ ) -> None:
284
+ """Render RFC 8628 device-code instructions inline."""
285
+ self._waiting_for_authorization = False
286
+ self._set_status(
287
+ f"Visit the URL below and enter the code (expires in {expires_in}s):",
288
+ )
289
+ if self._link_widget is not None:
290
+ self._link_widget.display = True
291
+ self._link_widget.update(
292
+ Content.assemble(
293
+ ("Verification URL: ", "bold"),
294
+ (verification_uri, TStyle(link=verification_uri, underline=True)),
295
+ ("\nUser code: ", "bold"),
296
+ (user_code, "bold"),
297
+ ),
298
+ )
299
+ self._append_history(
300
+ f"Device code: visit {verification_uri} and enter {user_code}",
301
+ )
302
+
303
+ async def show_success(self, message: str) -> None:
304
+ """Render a success status line without leaving OAuth fallback UI behind."""
305
+ self._waiting_for_authorization = False
306
+ self._hide_authorize_url()
307
+ self._set_status(message)
308
+
309
+ async def show_notice(self, message: str) -> None:
310
+ """Append a progress notice without disrupting the active prompt."""
311
+ self._append_history(message)
312
+
313
+ async def show_error(self, message: str) -> None:
314
+ """Render a fatal (flow-ending) error status line and history entry."""
315
+ self._waiting_for_authorization = False
316
+ self._set_status(message)
317
+ self._append_history(message)
318
+
319
+ # ------------------------------------------------------------------
320
+ # Internal helpers.
321
+ # ------------------------------------------------------------------
322
+
323
+ def _hide_authorize_url(self) -> None:
324
+ """Hide any OAuth authorization URL or fallback affordance."""
325
+ self._authorize_url = None
326
+ self._authorize_url_opened_in_browser = False
327
+ self._authorize_url_expanded = False
328
+ self._waiting_for_authorization = False
329
+ self._render_authorize_url()
330
+ self._set_help("Esc to cancel")
331
+
332
+ def _toggle_authorize_url(self) -> None:
333
+ """Toggle the manual authorize URL when the fallback affordance is active."""
334
+ if (
335
+ self._pending_input is not None
336
+ or self._authorize_url is None
337
+ or not self._authorize_url_opened_in_browser
338
+ ):
339
+ return
340
+ self._authorize_url_expanded = not self._authorize_url_expanded
341
+ self._render_authorize_url()
342
+
343
+ def _render_authorize_url(self) -> None:
344
+ """Render the manual authorize URL affordance."""
345
+ if self._link_widget is None:
346
+ return
347
+ url = self._authorize_url
348
+ if url is None:
349
+ self._link_widget.display = False
350
+ self._link_widget.update("")
351
+ return
352
+ self._link_widget.display = True
353
+ glyphs = get_glyphs()
354
+ if self._authorize_url_opened_in_browser and not self._authorize_url_expanded:
355
+ self._link_widget.update(
356
+ Content.assemble(
357
+ ("Having trouble? Show manual authorization URL ", "dim"),
358
+ (glyphs.cursor, "dim"),
359
+ ),
360
+ )
361
+ self._set_help("Enter to show URL · Esc to cancel")
362
+ return
363
+ prefix = (
364
+ f"Having trouble? Hide manual authorization URL {glyphs.arrow_down}\n"
365
+ if self._authorize_url_opened_in_browser
366
+ else "Authorization URL:\n"
367
+ )
368
+ self._link_widget.update(
369
+ Content.assemble(
370
+ (prefix, "bold"),
371
+ (url, TStyle(link=url, underline=True)),
372
+ ),
373
+ )
374
+ if self._authorize_url_opened_in_browser:
375
+ self._set_help("Enter to hide URL · Esc to cancel")
376
+ else:
377
+ self._set_help("Esc to cancel")
378
+
379
+ def _render_authorization_wait_status(self, frame: str) -> None:
380
+ """Render the browser-opened waiting state with an animated status line."""
381
+ self._set_status(
382
+ f"We opened your browser to connect {self._server_name}.\n"
383
+ "Complete authorization there to continue.\n\n"
384
+ f"Status: {frame} Waiting…",
385
+ )
386
+
387
+ def _set_status(self, message: str) -> None:
388
+ """Update the top status line."""
389
+ self._status = message
390
+ if self._status_widget is not None:
391
+ self._status_widget.update(Content.from_markup("$msg", msg=message))
392
+
393
+ def _set_help(self, message: str) -> None:
394
+ """Update the footer help text."""
395
+ if self._help_widget is not None:
396
+ self._help_widget.update(message)
397
+
398
+ def _hide_history(self) -> None:
399
+ """Hide the history pane for clean terminal states."""
400
+ if self._history_widget is not None:
401
+ self._history_widget.display = False
402
+ self._last_history_line = None
403
+
404
+ def _append_history(self, line: str) -> None:
405
+ """Append a line to the scrolling history pane."""
406
+ if self._history_widget is None or line == self._last_history_line:
407
+ return
408
+ self._last_history_line = line
409
+ self._history_widget.display = True
410
+ self._history_widget.mount(
411
+ Static(line, classes="ml-history-line", markup=False)
412
+ )
413
+ self._history_widget.scroll_end(animate=False)
414
+
415
+ async def _await_input(self, prompt: str) -> str:
416
+ """Show `prompt`, wait for `Enter`, and return the typed value.
417
+
418
+ Returns:
419
+ The raw input value the user submitted.
420
+
421
+ Raises:
422
+ MCPLoginCancelledError: When the modal is cancelled before or
423
+ during submission.
424
+ RuntimeError: When a concurrent prompt is already active.
425
+ """
426
+ if self._cancelled:
427
+ msg = "MCP login was cancelled before the prompt could be shown."
428
+ raise MCPLoginCancelledError(msg)
429
+ if self._pending_input is not None:
430
+ msg = (
431
+ "MCP login modal cannot have two concurrent input prompts; "
432
+ "the previous prompt was not resolved."
433
+ )
434
+ raise RuntimeError(msg)
435
+
436
+ loop = asyncio.get_running_loop()
437
+ future: asyncio.Future[str] = loop.create_future()
438
+ self._pending_input = future
439
+ if self._prompt_widget is not None:
440
+ self._prompt_widget.display = True
441
+ self._prompt_widget.update(prompt)
442
+ if self._input_widget is not None:
443
+ self._input_widget.value = ""
444
+ self._input_widget.display = True
445
+ self._input_widget.focus()
446
+ try:
447
+ return await future
448
+ finally:
449
+ self._pending_input = None
450
+ if self._input_widget is not None:
451
+ self._input_widget.display = False
452
+ if self._prompt_widget is not None:
453
+ self._prompt_widget.display = False
454
+
455
+ def on_input_submitted(self, event: Input.Submitted) -> None:
456
+ """Resolve the active prompt with the submitted value."""
457
+ if event.input.id != "ml-input":
458
+ return
459
+ future = self._pending_input
460
+ if future is not None and not future.done():
461
+ future.set_result(event.value)
462
+
463
+ def action_toggle_authorize_url(self) -> None:
464
+ """Toggle the manual authorize URL fallback via Enter."""
465
+ self._toggle_authorize_url()
466
+
467
+ def action_cancel(self) -> None:
468
+ """Cancel the login flow.
469
+
470
+ Sets a cancelled flag, completes any outstanding prompt with
471
+ `MCPLoginCancelledError` so the worker unblocks, and dismisses
472
+ the modal with the `cancelled` outcome.
473
+ """
474
+ if self._done:
475
+ return
476
+ self._cancelled = True
477
+ self._done = True
478
+ self._outcome = "cancelled"
479
+ future = self._pending_input
480
+ if future is not None and not future.done():
481
+ future.set_exception(
482
+ MCPLoginCancelledError("MCP login was cancelled by the user.")
483
+ )
484
+ self._stop_spinner_timer()
485
+ self.dismiss("cancelled")
486
+
487
+ def finish(self, *, success: bool, message: str | None = None) -> None:
488
+ """Close the modal from the worker, reporting the final outcome.
489
+
490
+ Dismiss is deferred by 0.6s so the user sees the final status
491
+ before the modal disappears.
492
+
493
+ Args:
494
+ success: `True` when login succeeded; drives the dismiss value.
495
+ message: Optional final status line shown before close.
496
+ """
497
+ if self._done:
498
+ return
499
+ self._done = True
500
+ self._outcome = "success" if success else "failed"
501
+ self._stop_spinner_timer()
502
+ self._waiting_for_authorization = False
503
+ if success:
504
+ self._hide_authorize_url()
505
+ self._hide_history()
506
+ if message is not None:
507
+ self._set_status(message)
508
+ if not success:
509
+ self._append_history(message)
510
+ glyphs = get_glyphs()
511
+ marker = glyphs.checkmark if success else glyphs.error
512
+ if self._title_widget is not None:
513
+ self._title_widget.update(
514
+ Content.from_markup(
515
+ "MCP login: $name $marker",
516
+ name=self._server_name,
517
+ marker=marker,
518
+ )
519
+ )
520
+
521
+ def _deferred_dismiss() -> None:
522
+ # Must be a def (not a lambda): Textual's `_invoke` auto-awaits
523
+ # any awaitable return, and `dismiss()` returns `AwaitComplete`;
524
+ # awaiting it inside the screen's own message pump raises
525
+ # `ScreenError`. A `None`-returning def discards the awaitable.
526
+ self.dismiss(self._outcome or "failed")
527
+
528
+ self.set_timer(0.6, _deferred_dismiss)
529
+
530
+ def _tick_spinner(self) -> None:
531
+ """Advance the status-line spinner while waiting for browser auth."""
532
+ if self._done or not self._waiting_for_authorization:
533
+ return
534
+ self._render_authorization_wait_status(self._spinner.next_frame())
535
+
536
+ def _stop_spinner_timer(self) -> None:
537
+ if self._spinner_timer is not None:
538
+ self._spinner_timer.stop()
539
+ self._spinner_timer = None