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,306 @@
1
+ """Ask user middleware for interactive question-answering during agent execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Annotated, Any, cast
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Awaitable, Callable
10
+
11
+
12
+ from langchain.agents.middleware.types import (
13
+ AgentMiddleware,
14
+ ContextT,
15
+ ModelRequest,
16
+ ModelResponse,
17
+ ResponseT,
18
+ )
19
+ from langchain.tools import InjectedToolCallId
20
+ from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
21
+ from langchain_core.tools import tool
22
+ from langgraph.types import Command, interrupt
23
+
24
+ from deepagents_code._ask_user_types import AskUserRequest, Question
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ ASK_USER_TOOL_DESCRIPTION = """Ask the user one or more questions when you need clarification or input before proceeding.
30
+
31
+ Each question can be either:
32
+ - "text": Free-form text response from the user
33
+ - "multiple_choice": User selects from predefined options (an "Other" option is always available)
34
+
35
+ For multiple choice questions, provide a list of choices. The user can pick one or type a custom answer via the "Other" option.
36
+
37
+ By default all questions are required. Set "required" to false for optional questions that the user can skip. Do not include "(required)", "(optional)", "- optional", or similar annotations in the question text — the UI renders that separately based on the "required" field.
38
+
39
+ Use this tool when:
40
+ - You need clarification on ambiguous requirements
41
+ - You want the user to choose between multiple valid approaches
42
+ - You need specific information only the user can provide
43
+ - You want to confirm a plan before executing it
44
+
45
+ Do NOT use this tool for:
46
+ - Simple yes/no confirmations (just proceed with your best judgment)
47
+ - Questions you can answer yourself from context
48
+ - Trivial decisions that don't meaningfully affect the outcome""" # noqa: E501
49
+
50
+ ASK_USER_SYSTEM_PROMPT = """## `ask_user`
51
+
52
+ You have access to the `ask_user` tool to ask the user questions when you need clarification or input.
53
+ Use this tool sparingly - only when you genuinely need information from the user that you cannot determine from context.
54
+
55
+ When using `ask_user`:
56
+ - Be concise and specific with your questions
57
+ - Use multiple choice when there are clear options to choose from
58
+ - Use text input when you need free-form responses
59
+ - Group related questions into a single ask_user call rather than making multiple calls
60
+ - Never ask questions you can answer yourself from the available context""" # noqa: E501
61
+
62
+
63
+ def _validate_questions(questions: list[Question]) -> None:
64
+ """Validate ask_user question structure before interrupting.
65
+
66
+ Args:
67
+ questions: Question definitions provided to the `ask_user` tool.
68
+
69
+ Raises:
70
+ ValueError: If the questions list or an individual question is invalid.
71
+ """
72
+ if not questions:
73
+ msg = "ask_user requires at least one question"
74
+ raise ValueError(msg)
75
+
76
+ for q in questions:
77
+ question_text = q.get("question")
78
+ if not isinstance(question_text, str) or not question_text.strip():
79
+ msg = "ask_user questions must have non-empty 'question' text"
80
+ raise ValueError(msg)
81
+
82
+ question_type = q.get("type")
83
+ if question_type not in {"text", "multiple_choice"}:
84
+ msg = f"unsupported ask_user question type: {question_type!r}"
85
+ raise ValueError(msg)
86
+
87
+ if question_type == "multiple_choice" and not q.get("choices"):
88
+ msg = (
89
+ f"multiple_choice question "
90
+ f"{q.get('question')!r} requires a "
91
+ f"non-empty 'choices' list"
92
+ )
93
+ raise ValueError(msg)
94
+
95
+ if question_type == "text" and q.get("choices"):
96
+ msg = f"text question {q.get('question')!r} must not define 'choices'"
97
+ raise ValueError(msg)
98
+
99
+
100
+ def _parse_answers(
101
+ response: object,
102
+ questions: list[Question],
103
+ tool_call_id: str,
104
+ ) -> Command[Any]:
105
+ """Parse an interrupt response into a `Command` with a `ToolMessage`.
106
+
107
+ Supports explicit status signaling from the adapter:
108
+
109
+ - `answered` (default): consume provided `answers`
110
+ - `cancelled`: synthesize `(cancelled)` answers
111
+ - `error`: synthesize `(error: ...)` answers
112
+
113
+ Malformed payloads are converted into explicit error answers instead of
114
+ silently defaulting to `(no answer)`.
115
+
116
+ Args:
117
+ response: Raw value returned by `interrupt()`.
118
+ questions: The questions that were asked.
119
+ tool_call_id: Originating tool call ID for the `ToolMessage`.
120
+
121
+ Returns:
122
+ `Command` containing a formatted `ToolMessage` with Q&A pairs.
123
+ """
124
+ status: str = "answered"
125
+ error_text: str | None = None
126
+ answers: list[str]
127
+ if not isinstance(response, dict):
128
+ logger.error(
129
+ "ask_user received malformed resume payload "
130
+ "(expected dict, got %s); returning explicit error answers",
131
+ type(response).__name__,
132
+ )
133
+ answers = []
134
+ status = "error"
135
+ error_text = "invalid ask_user response payload"
136
+ else:
137
+ response_dict = cast("dict[str, Any]", response)
138
+ response_status = response_dict.get("status")
139
+ if isinstance(response_status, str):
140
+ status = response_status
141
+
142
+ if "answers" not in response_dict:
143
+ if status == "answered":
144
+ logger.error(
145
+ "ask_user received resume payload without 'answers'; "
146
+ "returning explicit error answers"
147
+ )
148
+ answers = []
149
+ status = "error"
150
+ error_text = "missing ask_user answers payload"
151
+ else:
152
+ answers = []
153
+ else:
154
+ raw_answers = response_dict["answers"]
155
+ if isinstance(raw_answers, list):
156
+ answers = [str(answer) for answer in raw_answers]
157
+ else:
158
+ logger.error(
159
+ "ask_user received non-list 'answers' payload (%s); "
160
+ "returning explicit error answers",
161
+ type(raw_answers).__name__,
162
+ )
163
+ answers = []
164
+ status = "error"
165
+ error_text = "invalid ask_user answers payload"
166
+
167
+ if status == "error":
168
+ response_error = response_dict.get("error")
169
+ if isinstance(response_error, str) and response_error:
170
+ error_text = response_error
171
+ elif status == "cancelled":
172
+ answers = ["(cancelled)" for _ in questions]
173
+ elif status == "answered":
174
+ if len(answers) != len(questions):
175
+ logger.warning(
176
+ "ask_user answer count mismatch: expected %d, got %d",
177
+ len(questions),
178
+ len(answers),
179
+ )
180
+ else:
181
+ logger.error(
182
+ "ask_user received unknown status %r; returning explicit error answers",
183
+ status,
184
+ )
185
+ answers = []
186
+ status = "error"
187
+ error_text = "invalid ask_user response status"
188
+
189
+ if status == "error":
190
+ detail = error_text or "ask_user interaction failed"
191
+ answers = [f"(error: {detail})" for _ in questions]
192
+
193
+ formatted_answers = []
194
+ for i, q in enumerate(questions):
195
+ answer = answers[i] if i < len(answers) else "(no answer)"
196
+ formatted_answers.append(f"Q: {q['question']}\nA: {answer}")
197
+ result_text = "\n\n".join(formatted_answers)
198
+ return Command(
199
+ update={
200
+ "messages": [ToolMessage(result_text, tool_call_id=tool_call_id)],
201
+ }
202
+ )
203
+
204
+
205
+ class AskUserMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
206
+ """Middleware that provides an ask_user tool for interactive questioning.
207
+
208
+ This middleware adds an `ask_user` tool that allows agents to ask the user
209
+ questions during execution. Questions can be free-form text or multiple choice.
210
+ The tool uses LangGraph interrupts to pause execution and wait for user input.
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ *,
216
+ system_prompt: str = ASK_USER_SYSTEM_PROMPT,
217
+ tool_description: str = ASK_USER_TOOL_DESCRIPTION,
218
+ ) -> None:
219
+ """Initialize AskUserMiddleware.
220
+
221
+ Args:
222
+ system_prompt: System-level instructions injected into every LLM
223
+ request to guide `ask_user` usage.
224
+ tool_description: Description string passed to the `ask_user` tool
225
+ decorator, visible to the LLM in the tool schema.
226
+ """
227
+ super().__init__()
228
+ self.system_prompt = system_prompt
229
+ self.tool_description = tool_description
230
+
231
+ @tool(description=self.tool_description)
232
+ def _ask_user(
233
+ questions: list[Question],
234
+ tool_call_id: Annotated[str, InjectedToolCallId],
235
+ ) -> Command[Any]:
236
+ """Ask the user one or more questions.
237
+
238
+ Args:
239
+ questions: Questions to present to the user.
240
+ tool_call_id: Tool call identifier injected by LangChain.
241
+
242
+ Returns:
243
+ `Command` containing the parsed user answers as a `ToolMessage`.
244
+ """
245
+ _validate_questions(questions)
246
+ ask_request = AskUserRequest(
247
+ type="ask_user",
248
+ questions=questions,
249
+ tool_call_id=tool_call_id,
250
+ )
251
+ # interrupt() raises GraphInterrupt from INSIDE tool execution,
252
+ # within ToolNode's wrap_tool_call chain. Any
253
+ # wrap_tool_call middleware that catches exceptions MUST re-raise
254
+ # GraphBubbleUp — a broad `except Exception` (e.g. ToolRetryMiddleware)
255
+ # would swallow this interrupt and silently break ask_user.
256
+ response = interrupt(ask_request)
257
+ return _parse_answers(response, questions, tool_call_id)
258
+
259
+ _ask_user.name = "ask_user"
260
+ self.tools = [_ask_user]
261
+
262
+ def wrap_model_call(
263
+ self,
264
+ request: ModelRequest[ContextT],
265
+ handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
266
+ ) -> ModelResponse[ResponseT] | AIMessage:
267
+ """Inject the ask_user system prompt.
268
+
269
+ Returns:
270
+ Model response from the wrapped handler.
271
+ """
272
+ if request.system_message is not None:
273
+ new_system_content = [
274
+ *request.system_message.content_blocks,
275
+ {"type": "text", "text": f"\n\n{self.system_prompt}"},
276
+ ]
277
+ else:
278
+ new_system_content = [{"type": "text", "text": self.system_prompt}]
279
+ new_system_message = SystemMessage(
280
+ content=cast("list[str | dict[str, str]]", new_system_content)
281
+ )
282
+ return handler(request.override(system_message=new_system_message))
283
+
284
+ async def awrap_model_call(
285
+ self,
286
+ request: ModelRequest[ContextT],
287
+ handler: Callable[
288
+ [ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
289
+ ],
290
+ ) -> ModelResponse[ResponseT] | AIMessage:
291
+ """Inject the ask_user system prompt (async).
292
+
293
+ Returns:
294
+ Model response from the wrapped handler.
295
+ """
296
+ if request.system_message is not None:
297
+ new_system_content = [
298
+ *request.system_message.content_blocks,
299
+ {"type": "text", "text": f"\n\n{self.system_prompt}"},
300
+ ]
301
+ else:
302
+ new_system_content = [{"type": "text", "text": self.system_prompt}]
303
+ new_system_message = SystemMessage(
304
+ content=cast("list[str | dict[str, str]]", new_system_content)
305
+ )
306
+ return await handler(request.override(system_message=new_system_message))
@@ -0,0 +1,185 @@
1
+ """Shared provider auth status formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, assert_never
6
+
7
+ from textual.content import Content
8
+
9
+ from deepagents_code.model_config import (
10
+ CODEX_PROVIDER,
11
+ ProviderAuthSource,
12
+ ProviderAuthState,
13
+ ProviderAuthStatus,
14
+ resolved_env_var_name,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from deepagents_code.config import Glyphs
19
+
20
+
21
+ def format_auth_badge(status: ProviderAuthStatus) -> Content:
22
+ """Format an auth manager badge for a provider.
23
+
24
+ Used by the `/auth` manager, where each provider renders a bracketed,
25
+ styled badge (e.g. `[stored]`, `[env set: ANTHROPIC_API_KEY]`, `[missing]`).
26
+
27
+ Args:
28
+ status: Provider auth/readiness status.
29
+
30
+ Returns:
31
+ A styled badge `Content` for the auth manager surface.
32
+ """
33
+ # The ChatGPT-OAuth codex provider has no API key, so its credential is a
34
+ # file-backed OAuth token rather than a stored key. Give it distinctive
35
+ # badges (`[chatgpt]` / `[sign in to chatgpt]`) so users don't read the
36
+ # generic `[stored]`/`[missing]` as a literal API key on disk.
37
+ if status.provider == CODEX_PROVIDER:
38
+ return _format_codex_badge(status)
39
+
40
+ state = status.state
41
+ match state:
42
+ case ProviderAuthState.CONFIGURED:
43
+ return _format_configured_badge(status)
44
+ case ProviderAuthState.MISSING:
45
+ return Content.styled("[missing]", "bold $warning")
46
+ case ProviderAuthState.NOT_REQUIRED:
47
+ return _auth_badge(status.detail or "no API key required")
48
+ case ProviderAuthState.IMPLICIT:
49
+ return _auth_badge(status.detail or "implicit auth")
50
+ case ProviderAuthState.MANAGED:
51
+ return _auth_badge(status.detail or "custom auth")
52
+ case ProviderAuthState.UNKNOWN:
53
+ return _auth_badge(status.detail or "credentials unknown", prefix="? ")
54
+ case _:
55
+ assert_never(state)
56
+
57
+
58
+ def format_auth_indicator(status: ProviderAuthStatus, glyphs: Glyphs) -> str:
59
+ """Format a model selector provider-header indicator.
60
+
61
+ Used by `/model`, where the indicator is plain text shown next to the
62
+ provider name. Returns an empty string for `CONFIGURED` providers, which
63
+ need no indicator.
64
+
65
+ Args:
66
+ status: Provider auth/readiness status.
67
+ glyphs: Glyph table for the active terminal mode.
68
+
69
+ Returns:
70
+ Text shown next to the provider name, or an empty string when no
71
+ indicator should be rendered (e.g., `CONFIGURED`).
72
+ """
73
+ state = status.state
74
+ match state:
75
+ case ProviderAuthState.CONFIGURED:
76
+ return ""
77
+ case ProviderAuthState.MISSING:
78
+ return f"{glyphs.warning} missing credentials"
79
+ case ProviderAuthState.NOT_REQUIRED:
80
+ return status.detail or "no API key required"
81
+ case ProviderAuthState.IMPLICIT:
82
+ return status.detail or "implicit auth"
83
+ case ProviderAuthState.MANAGED:
84
+ return status.detail or "custom auth"
85
+ case ProviderAuthState.UNKNOWN:
86
+ detail = status.detail or "credentials unknown"
87
+ return f"{glyphs.question} {detail}"
88
+ case _:
89
+ assert_never(state)
90
+
91
+
92
+ def _auth_badge(detail: str, *, prefix: str = "") -> Content:
93
+ """Format a muted auth manager badge.
94
+
95
+ Args:
96
+ detail: Badge text inside the brackets.
97
+ prefix: Text prepended verbatim before `detail` inside the brackets.
98
+ Callers include any separator themselves (e.g. `"? "`); this
99
+ helper does not insert one.
100
+
101
+ Returns:
102
+ Formatted auth manager badge content.
103
+ """
104
+ return Content.assemble(
105
+ ("[", "$text-muted"),
106
+ (prefix, "$text-muted"),
107
+ Content.styled(detail, "$text-muted"),
108
+ ("]", "$text-muted"),
109
+ )
110
+
111
+
112
+ def _format_codex_badge(status: ProviderAuthStatus) -> Content:
113
+ """Format the auth manager badge for the ChatGPT-OAuth codex provider.
114
+
115
+ Codex signs in through ChatGPT OAuth rather than an API key, so a
116
+ `CONFIGURED` status renders as `[chatgpt]` (carrying the plan name when the
117
+ status detail includes one) and any other state renders as a
118
+ `[sign in to chatgpt]` prompt.
119
+
120
+ Args:
121
+ status: The codex provider's auth status.
122
+
123
+ Returns:
124
+ A styled badge reflecting ChatGPT sign-in state.
125
+ """
126
+ if status.state is ProviderAuthState.CONFIGURED:
127
+ badge_text = "[chatgpt]"
128
+ # `_get_codex_auth_status` encodes the plan as a trailing "(plan)" in
129
+ # the detail string (e.g. "signed in to ChatGPT (pro)").
130
+ if status.detail and (plan := _codex_plan_from_detail(status.detail)):
131
+ badge_text = f"[chatgpt: {plan}]"
132
+ return Content.styled(badge_text, "bold $success")
133
+ return Content.styled("[sign in to chatgpt]", "bold $warning")
134
+
135
+
136
+ def _codex_plan_from_detail(detail: str) -> str | None:
137
+ """Extract the ChatGPT plan from a codex auth detail string.
138
+
139
+ Args:
140
+ detail: Human-readable codex auth status detail.
141
+
142
+ Returns:
143
+ The ChatGPT plan text, or `None` when no bounded plan is present.
144
+ """
145
+ _, marker, plan_tail = detail.partition("(")
146
+ if not marker:
147
+ return None
148
+ plan, marker, _ = plan_tail.partition(")")
149
+ if not marker or not plan:
150
+ return None
151
+ return plan
152
+
153
+
154
+ def _format_configured_badge(status: ProviderAuthStatus) -> Content:
155
+ """Format the auth manager badge for a `CONFIGURED` provider.
156
+
157
+ Args:
158
+ status: A `CONFIGURED` provider auth status.
159
+
160
+ Returns:
161
+ A styled badge naming the credential source (`[stored]` or `[env set: …]`).
162
+
163
+ Raises:
164
+ ValueError: If the status carries no source. `ProviderAuthStatus`
165
+ guarantees `CONFIGURED` implies a source, so this guards against
166
+ that invariant being violated rather than a normal input.
167
+ """
168
+ match status.source:
169
+ case ProviderAuthSource.STORED:
170
+ return Content.styled("[stored]", "bold $success")
171
+ case ProviderAuthSource.ENV:
172
+ if status.env_var:
173
+ return Content.assemble(
174
+ ("[env set: ", "$text-muted"),
175
+ Content.styled(
176
+ resolved_env_var_name(status.env_var), "$text-muted"
177
+ ),
178
+ ("]", "$text-muted"),
179
+ )
180
+ return Content.styled("[env]", "$text-muted")
181
+ case None:
182
+ msg = f"CONFIGURED auth status has no source: {status!r}"
183
+ raise ValueError(msg)
184
+ case _:
185
+ assert_never(status.source)