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,891 @@
1
+ """LangChain brand colors and semantic constants for the app.
2
+
3
+ Single source of truth for color values used in Python code (Rich markup,
4
+ `Content.styled`, `Content.from_markup`). CSS-side styling should reference
5
+ Textual CSS variables: built-in variables
6
+ (`$primary`, `$background`, `$text-muted`, `$error-muted`, etc.) are set via
7
+ `register_theme()` in `DeepAgentsApp.__init__`, while the few app-specific
8
+ variables (`$mode-bash`, `$mode-command`, `$mode-incognito`, `$skill`,
9
+ `$skill-hover`, `$tool`, `$tool-hover`) are backed by these constants via
10
+ `App.get_theme_variable_defaults()`.
11
+
12
+ Code that needs custom CSS variable values should call
13
+ `get_css_variable_defaults(dark=...)`. For the full semantic color palette, look
14
+ up the `ThemeColors` instance via `get_registry()`.
15
+
16
+ Users can define custom themes in `~/.deepagents/config.toml` under
17
+ `[themes.<name>]` sections. Each new theme section must include `label` (str);
18
+ `dark` (bool) defaults to `False` if omitted (set to `True` for dark themes).
19
+ Color fields are optional and fall back to the built-in dark/light palette based
20
+ on the `dark` flag. Sections whose name matches a built-in theme override its
21
+ colors without replacing it. See `_load_user_themes()` for details.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import functools
27
+ import logging
28
+ import re
29
+ from dataclasses import dataclass, fields
30
+ from pathlib import Path
31
+ from types import MappingProxyType
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Mapping
36
+
37
+ from textual.app import App
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Brand palette — dark (originally tokyonight-inspired, LangChain blue primary)
43
+ # ---------------------------------------------------------------------------
44
+ LC_DARK = "#11121D"
45
+ """Background — visible blue tint, distinguishable from pure black."""
46
+
47
+ LC_CARD = "#1A1B2E"
48
+ """Surface / card — clearly elevated above background."""
49
+
50
+ LC_BORDER_DK = "#25283B"
51
+ """Borders on dark backgrounds."""
52
+
53
+ LC_BORDER_LT = "#3A3E57"
54
+ """Borders on lighter / hovered backgrounds."""
55
+
56
+ LC_BODY = "#C0CAF5"
57
+ """Body text — high contrast on dark backgrounds."""
58
+
59
+ LC_BLUE = "#7AA2F7"
60
+ """Primary accent blue."""
61
+
62
+ LC_PURPLE = "#BB9AF7"
63
+ """Secondary accent / badges / labels."""
64
+
65
+ LC_GREEN = "#9ECE6A"
66
+ """Success / positive indicator."""
67
+
68
+ LC_AMBER = "#EB8B46"
69
+ """Warning / caution indicator."""
70
+
71
+ LC_PINK = "#F7768E"
72
+ """Error / destructive actions."""
73
+
74
+ LC_MUTED = "#545C7E"
75
+ """Muted / secondary text."""
76
+
77
+ LC_GREEN_BG = "#1C2A38"
78
+ """Subtle green-tinted background for diff additions."""
79
+
80
+ LC_PINK_BG = "#2A1F32"
81
+ """Subtle pink-tinted background for diff removals / errors."""
82
+
83
+ LC_PANEL = "#25283B"
84
+ """Panel — differentiated section background (above surface)."""
85
+
86
+ LC_SKILL = "#A78BFA"
87
+ """Skill invocation accent — border and header text."""
88
+
89
+ LC_SKILL_HOVER = "#C4B5FD"
90
+ """Skill invocation hover — lighter variant for interactive feedback."""
91
+
92
+ LC_TOOL = LC_AMBER
93
+ """Tool call accent — border and header text."""
94
+
95
+ LC_TOOL_HOVER = "#FFCB91"
96
+ """Tool call hover — lighter variant for interactive feedback."""
97
+
98
+ LC_INCOGNITO = "#2DD4BF"
99
+ """Incognito shell accent — teal, must read distinctly against `LC_PINK`
100
+ shell and `error` warning border colors."""
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Brand palette — light
105
+ # ---------------------------------------------------------------------------
106
+ LC_LIGHT_BG = "#F5F5F7"
107
+ """Background — warm neutral white."""
108
+
109
+ LC_LIGHT_SURFACE = "#EAEAEE"
110
+ """Surface / card — slightly darker than background."""
111
+
112
+ LC_LIGHT_BORDER = "#C8CAD0"
113
+ """Borders on light backgrounds."""
114
+
115
+ LC_LIGHT_BORDER_HVR = "#A0A4B0"
116
+ """Borders on hovered / focused surfaces."""
117
+
118
+ LC_LIGHT_BODY = "#24283B"
119
+ """Body text — high contrast on light backgrounds."""
120
+
121
+ LC_LIGHT_BLUE = "#2E5EAA"
122
+ """Primary accent blue (darkened for light bg contrast)."""
123
+
124
+ LC_LIGHT_PURPLE = "#7C3AED"
125
+ """Secondary accent (darkened for light bg contrast)."""
126
+
127
+ LC_LIGHT_GREEN = "#3A7D0A"
128
+ """Success / positive (darkened for light bg contrast)."""
129
+
130
+ LC_LIGHT_AMBER = "#B45309"
131
+ """Warning / caution (darkened for light bg contrast)."""
132
+
133
+ LC_LIGHT_PINK = "#BE185D"
134
+ """Error / destructive (darkened for light bg contrast)."""
135
+
136
+ LC_LIGHT_MUTED = "#6B7280"
137
+ """Muted / secondary text on light backgrounds."""
138
+
139
+ LC_LIGHT_GREEN_BG = "#DCFCE7"
140
+ """Subtle green-tinted background for diff additions."""
141
+
142
+ LC_LIGHT_PINK_BG = "#FEE2E2"
143
+ """Subtle pink-tinted background for diff removals / errors."""
144
+
145
+ LC_LIGHT_PANEL = "#E0E1E6"
146
+ """Panel for light theme — differentiated section background."""
147
+
148
+ LC_LIGHT_SKILL = "#7C3AED"
149
+ """Skill invocation accent (darkened for light bg contrast)."""
150
+
151
+ LC_LIGHT_SKILL_HOVER = "#6D28D9"
152
+ """Skill invocation hover (darkened for light bg contrast)."""
153
+
154
+ LC_LIGHT_TOOL = LC_LIGHT_AMBER
155
+ """Tool call accent (darkened for light bg contrast)."""
156
+
157
+ LC_LIGHT_TOOL_HOVER = "#78350F"
158
+ """Tool call hover (darkened for light bg contrast)."""
159
+
160
+ LC_LIGHT_INCOGNITO = "#0F766E"
161
+ """Incognito shell accent (darkened for light bg contrast)."""
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Semantic constants (ANSI color names for Rich console output)
166
+ #
167
+ # These are ANSI color names resolved by the user's terminal palette, so they
168
+ # adapt to both dark and light terminal backgrounds automatically. They are
169
+ # used in Rich's `Console.print()` (non-interactive output, help screens,
170
+ # `non_interactive.py`, `main.py`).
171
+ #
172
+ # Textual widget code should NOT use these. Instead, call
173
+ # `get_theme_colors(self.app)` to obtain the active theme's `ThemeColors`
174
+ # (hex values), or reference CSS variables (`$primary`, `$muted`, etc.).
175
+ # ---------------------------------------------------------------------------
176
+ PRIMARY = "blue"
177
+ """Default accent for headings, borders, links, and active elements."""
178
+
179
+ PRIMARY_DEV = "bright_red"
180
+ """Accent used when running from an editable (dev) install."""
181
+
182
+ SUCCESS = "green"
183
+ """Positive outcomes — tool success, approved actions."""
184
+
185
+ WARNING = "yellow"
186
+ """Caution and notice states — auto-approve off, pending tool calls, notices."""
187
+
188
+ MUTED = "bright_black"
189
+ """De-emphasized text — timestamps, secondary labels."""
190
+
191
+ MODE_BASH = "red"
192
+ """Shell mode indicator — borders, prompts, and message prefixes."""
193
+
194
+ MODE_COMMAND = "magenta"
195
+ """Command mode indicator — borders, prompts, and message prefixes."""
196
+
197
+ # Diff colors
198
+ DIFF_ADD_FG = "green"
199
+ """Added-line foreground in inline diffs."""
200
+
201
+ DIFF_ADD_BG = "green"
202
+ """Added-line background in inline diffs."""
203
+
204
+ DIFF_REMOVE_FG = "red"
205
+ """Removed-line foreground in inline diffs."""
206
+
207
+ DIFF_REMOVE_BG = "red"
208
+ """Removed-line background in inline diffs."""
209
+
210
+ DIFF_CONTEXT = "bright_black"
211
+ """Unchanged context lines in inline diffs."""
212
+
213
+ # Tool call widget
214
+ TOOL_BORDER = "bright_black"
215
+ """Tool call card border."""
216
+
217
+ TOOL_HEADER = "yellow"
218
+ """Tool call headers, slash-command tokens, and approval-menu commands."""
219
+
220
+ # File listing colors
221
+ FILE_PYTHON = "blue"
222
+ """Python files in tool-call file listings."""
223
+
224
+ FILE_CONFIG = "yellow"
225
+ """Config / data files in tool-call file listings."""
226
+
227
+ FILE_DIR = "green"
228
+ """Directories in tool-call file listings."""
229
+
230
+ SPINNER = "blue"
231
+ """Loading spinner color."""
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Theme variant dataclass
236
+ # ---------------------------------------------------------------------------
237
+
238
+
239
+ _textual_colors_cache: dict[tuple[str, bool], ThemeColors] = {}
240
+ """Cache of derived built-in `ThemeColors` keyed on `(theme name, dark)`.
241
+
242
+ A built-in Textual theme does not change its color values once registered under
243
+ a name, so its derived colors only change when the active theme changes. Caching
244
+ avoids re-running over a dozen hex validations on every widget render. The `dark`
245
+ flag is part of the key defensively; for built-in themes it is already fixed by
246
+ the name. Only registered built-ins are cached (see `get_theme_colors`) — the
247
+ cache is cleared by `reload_registry`.
248
+ """
249
+
250
+
251
+ _HEX_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
252
+ """Matches a 7-character hex color string like `#7AA2F7`.
253
+
254
+ Textual's `Color.parse` could also validate, but importing it here would pull
255
+ Textual into `theme.py` which is otherwise pure Python with zero framework deps.
256
+ """
257
+
258
+
259
+ @dataclass(frozen=True, slots=True)
260
+ class ThemeColors:
261
+ """Complete set of semantic colors for one theme variant.
262
+
263
+ Every field must be a 7-character hex color string (e.g., `'#7AA2F7'`).
264
+ """
265
+
266
+ primary: str
267
+ """Accent for headings, borders, links, and active elements."""
268
+
269
+ secondary: str
270
+ """Secondary accent for badges, labels, and decorative highlights."""
271
+
272
+ accent: str
273
+ """Attention-drawing contrast accent, distinct from primary/secondary."""
274
+
275
+ panel: str
276
+ """Differentiated section background (above surface)."""
277
+
278
+ success: str
279
+ """Positive outcomes — tool success, approved actions."""
280
+
281
+ warning: str
282
+ """Caution and notice states — pending tool calls, notices."""
283
+
284
+ error: str
285
+ """Error and destructive-action indicator."""
286
+
287
+ muted: str
288
+ """De-emphasized text — timestamps, secondary labels."""
289
+
290
+ mode_bash: str
291
+ """Shell mode indicator — borders, prompts, and message prefixes."""
292
+
293
+ mode_command: str
294
+ """Command mode indicator — borders, prompts, and message prefixes."""
295
+
296
+ mode_incognito: str
297
+ """Incognito shell indicator — borders, prompts, and message prefixes."""
298
+
299
+ skill: str
300
+ """Skill invocation accent — border and header text."""
301
+
302
+ skill_hover: str
303
+ """Skill invocation hover — contrasting variant for interactive feedback."""
304
+
305
+ tool: str
306
+ """Tool call accent — border and header text."""
307
+
308
+ tool_hover: str
309
+ """Tool call hover — contrasting variant for interactive feedback."""
310
+
311
+ foreground: str
312
+ """Primary body text."""
313
+
314
+ background: str
315
+ """Base application background."""
316
+
317
+ surface: str
318
+ """Elevated card / panel background."""
319
+
320
+ def __post_init__(self) -> None:
321
+ """Validate that every field is a valid hex color.
322
+
323
+ Raises:
324
+ ValueError: If any field is not a 7-character hex color string.
325
+ """
326
+ for f in fields(self):
327
+ val = getattr(self, f.name)
328
+ if not _HEX_RE.match(val):
329
+ msg = (
330
+ f"ThemeColors.{f.name} must be a 7-char hex color"
331
+ f" (#RRGGBB), got {val!r}"
332
+ )
333
+ raise ValueError(msg)
334
+
335
+ @classmethod
336
+ def merged(cls, base: ThemeColors, overrides: dict[str, str]) -> ThemeColors:
337
+ """Create a new `ThemeColors` by overlaying overrides onto a base.
338
+
339
+ Fields present in `overrides` replace the corresponding base value;
340
+ missing fields inherit from `base`. This lets users specify only the
341
+ colors they want to customize.
342
+
343
+ Args:
344
+ base: Fallback color set for any field not in `overrides`.
345
+ overrides: Field-name to hex-color mapping. Unknown keys are
346
+ silently ignored.
347
+
348
+ Returns:
349
+ New `ThemeColors` with merged values.
350
+ """
351
+ valid_names = {f.name for f in fields(cls)}
352
+ kwargs = {f.name: getattr(base, f.name) for f in fields(cls)}
353
+ kwargs.update({k: v for k, v in overrides.items() if k in valid_names})
354
+ return cls(**kwargs)
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Built-in theme color sets
359
+ # ---------------------------------------------------------------------------
360
+
361
+ DARK_COLORS = ThemeColors(
362
+ primary=LC_BLUE,
363
+ secondary=LC_PURPLE,
364
+ accent=LC_GREEN,
365
+ panel=LC_PANEL,
366
+ success=LC_GREEN,
367
+ warning=LC_AMBER,
368
+ error=LC_PINK,
369
+ muted=LC_MUTED,
370
+ mode_bash=LC_PINK,
371
+ mode_command=LC_PURPLE,
372
+ mode_incognito=LC_INCOGNITO,
373
+ skill=LC_SKILL,
374
+ skill_hover=LC_SKILL_HOVER,
375
+ tool=LC_TOOL,
376
+ tool_hover=LC_TOOL_HOVER,
377
+ foreground=LC_BODY,
378
+ background=LC_DARK,
379
+ surface=LC_CARD,
380
+ )
381
+ """Color set for the dark LangChain theme."""
382
+
383
+ LIGHT_COLORS = ThemeColors(
384
+ primary=LC_LIGHT_BLUE,
385
+ secondary=LC_LIGHT_PURPLE,
386
+ accent=LC_LIGHT_GREEN,
387
+ panel=LC_LIGHT_PANEL,
388
+ success=LC_LIGHT_GREEN,
389
+ warning=LC_LIGHT_AMBER,
390
+ error=LC_LIGHT_PINK,
391
+ muted=LC_LIGHT_MUTED,
392
+ mode_bash=LC_LIGHT_PINK,
393
+ mode_command=LC_LIGHT_PURPLE,
394
+ mode_incognito=LC_LIGHT_INCOGNITO,
395
+ skill=LC_LIGHT_SKILL,
396
+ skill_hover=LC_LIGHT_SKILL_HOVER,
397
+ tool=LC_LIGHT_TOOL,
398
+ tool_hover=LC_LIGHT_TOOL_HOVER,
399
+ foreground=LC_LIGHT_BODY,
400
+ background=LC_LIGHT_BG,
401
+ surface=LC_LIGHT_SURFACE,
402
+ )
403
+ """Color set for the light LangChain theme."""
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Available themes (name → display label, dark flag, colors)
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ @dataclass(frozen=True, slots=True)
412
+ class ThemeEntry:
413
+ """Metadata for a registered theme."""
414
+
415
+ label: str
416
+ """Human-readable label shown in the theme picker."""
417
+
418
+ dark: bool
419
+ """Whether this is a dark theme variant."""
420
+
421
+ colors: ThemeColors
422
+ """Resolved color set."""
423
+
424
+ custom: bool = True
425
+ """Whether this theme must be registered with Textual via `register_theme()`.
426
+
427
+ `True` for LangChain-branded themes and user-defined themes.
428
+ `False` for Textual built-in themes that Textual already knows about.
429
+ """
430
+
431
+ def __post_init__(self) -> None:
432
+ """Validate that the label is a non-empty string.
433
+
434
+ Raises:
435
+ ValueError: If `label` is empty or whitespace-only.
436
+ """
437
+ if not self.label.strip():
438
+ msg = "ThemeEntry.label must be a non-empty string"
439
+ raise ValueError(msg)
440
+
441
+
442
+ # Curated labels for Textual built-in themes. Themes not listed here fall back
443
+ # to a humanized version of the slug (e.g. `ansi-dark` → `Ansi Dark`), so newly
444
+ # shipped Textual themes appear in the picker without code changes.
445
+ _TEXTUAL_THEME_LABELS: Mapping[str, str] = MappingProxyType(
446
+ {
447
+ "textual-dark": "Textual Dark",
448
+ "textual-light": "Textual Light",
449
+ "ansi-dark": "Terminal ANSI Dark",
450
+ "ansi-light": "Terminal ANSI Light",
451
+ "catppuccin-frappe": "Catppuccin Frappé",
452
+ "rose-pine": "Rosé Pine",
453
+ "rose-pine-dawn": "Rosé Pine Dawn",
454
+ "rose-pine-moon": "Rosé Pine Moon",
455
+ "tokyo-night": "Tokyo Night",
456
+ }
457
+ )
458
+
459
+
460
+ def _builtin_themes() -> dict[str, ThemeEntry]:
461
+ """Return the built-in theme entries as a mutable dict.
462
+
463
+ Textual built-ins are discovered from `textual.theme.BUILTIN_THEMES` so
464
+ newly shipped Textual themes appear automatically. They are not registered
465
+ via `register_theme()` — Textual's own `$primary`, `$background`, etc.
466
+ apply. The `colors` field provides fallback values for app-specific CSS
467
+ vars (`$mode-bash`, `$mode-command`, `$mode-incognito`) and Python-side
468
+ styling. For standard properties (primary, secondary, etc.),
469
+ `get_theme_colors()` dynamically
470
+ resolves from the actual Textual theme at runtime so the Python and CSS
471
+ color systems stay in sync.
472
+
473
+ Returns:
474
+ Dict of built-in theme names to `ThemeEntry` instances.
475
+ """
476
+ r: dict[str, ThemeEntry] = {}
477
+ r["langchain"] = ThemeEntry(
478
+ label="LangChain Dark",
479
+ dark=True,
480
+ colors=DARK_COLORS,
481
+ )
482
+ r["langchain-light"] = ThemeEntry(
483
+ label="LangChain Light",
484
+ dark=False,
485
+ colors=LIGHT_COLORS,
486
+ )
487
+
488
+ from textual.theme import BUILTIN_THEMES
489
+
490
+ for name, builtin in BUILTIN_THEMES.items():
491
+ label = _TEXTUAL_THEME_LABELS.get(name) or name.replace("-", " ").title()
492
+ r[name] = ThemeEntry(
493
+ label=label,
494
+ dark=builtin.dark,
495
+ colors=DARK_COLORS if builtin.dark else LIGHT_COLORS,
496
+ custom=False,
497
+ )
498
+ return r
499
+
500
+
501
+ @functools.cache
502
+ def _builtin_names() -> frozenset[str]:
503
+ """Names of built-in themes; lazily computed and cached.
504
+
505
+ User `[themes.<name>]` sections matching a built-in name override its colors
506
+ rather than creating a new theme. Derived from `_builtin_themes()` so the
507
+ set stays in sync automatically. Lazy because `_builtin_themes()` imports
508
+ `textual.theme.BUILTIN_THEMES`, and we don't want to pull Textual onto the
509
+ `deepagents --help` / `deepagents -v` cold-start path.
510
+
511
+ Returns:
512
+ Frozen set of built-in theme names.
513
+ """
514
+ return frozenset(_builtin_themes())
515
+
516
+
517
+ def _load_user_themes(
518
+ builtins: dict[str, ThemeEntry],
519
+ *,
520
+ config_path: Path | None = None,
521
+ ) -> None:
522
+ """Load user-defined themes from `config.toml` into `builtins` (mutated).
523
+
524
+ **New themes** — each `[themes.<name>]` section (where `<name>` is not a
525
+ built-in) must have:
526
+
527
+ - `label` (str) — human-readable name shown in the theme picker.
528
+ - `dark` (bool, optional) — whether this is a dark-mode variant.
529
+
530
+ Defaults to `False` (light).
531
+
532
+ **Built-in overrides** — if `<name>` matches a built-in theme, only color
533
+ fields are read; `label` and `dark` are inherited from the built-in.
534
+
535
+ All `ThemeColors` fields are optional. For new themes, omitted fields
536
+ fall back to the built-in dark or light palette based on the `dark` flag.
537
+
538
+ For built-in overrides, omitted fields retain the existing built-in colors.
539
+
540
+ Invalid themes (bad hex, missing required keys) are logged as warnings
541
+ and skipped — they never crash startup.
542
+
543
+ Example `config.toml` snippet:
544
+
545
+ ```toml
546
+ # New custom theme
547
+ [themes.my-solarized]
548
+ label = "My Solarized"
549
+ dark = true
550
+ primary = "#268BD2"
551
+ warning = "#B58900"
552
+
553
+ # Override built-in theme colors
554
+ [themes.langchain]
555
+ primary = "#FF5500"
556
+ ```
557
+
558
+ Args:
559
+ builtins: Mutable dict to update (new themes are added, built-in
560
+ overrides replace existing entries).
561
+ config_path: Override for the config file path (testing).
562
+ """
563
+ if config_path is None:
564
+ try:
565
+ config_path = Path.home() / ".zjcode" / "config.toml"
566
+ except RuntimeError:
567
+ logger.debug("Cannot determine home directory; skipping user theme loading")
568
+ return
569
+
570
+ import tomllib
571
+
572
+ try:
573
+ if not config_path.exists():
574
+ return
575
+
576
+ with config_path.open("rb") as f:
577
+ data = tomllib.load(f)
578
+ except (tomllib.TOMLDecodeError, PermissionError, OSError) as exc:
579
+ logger.warning(
580
+ "Could not read %s for user themes: %s",
581
+ config_path,
582
+ exc,
583
+ )
584
+ return
585
+
586
+ themes_section: Any = data.get("themes")
587
+ if not isinstance(themes_section, dict) or not themes_section:
588
+ return
589
+
590
+ valid_color_names = {f.name for f in fields(ThemeColors)}
591
+ reserved = {"label", "dark"}
592
+
593
+ for name, section in themes_section.items():
594
+ if not isinstance(section, dict):
595
+ logger.warning("Ignoring non-table [themes.%s]", name)
596
+ continue
597
+
598
+ # --- Parse color overrides (shared by built-in overrides & new themes)
599
+ color_overrides: dict[str, str] = {}
600
+ for k, v in section.items():
601
+ if k in reserved:
602
+ continue
603
+ if not isinstance(v, str):
604
+ logger.warning(
605
+ "User theme '%s' field '%s' must be a string, got %s; ignoring",
606
+ name,
607
+ k,
608
+ type(v).__name__,
609
+ )
610
+ continue
611
+ if k in valid_color_names:
612
+ color_overrides[k] = v
613
+ else:
614
+ logger.warning(
615
+ "User theme '%s' has unknown color field '%s'; ignoring",
616
+ name,
617
+ k,
618
+ )
619
+
620
+ # --- Built-in override: merge color tweaks into the existing entry
621
+ if name in _builtin_names():
622
+ existing = builtins.get(name)
623
+ if existing is None:
624
+ logger.warning(
625
+ "Built-in theme '%s' not in builtins dict; skipping override",
626
+ name,
627
+ )
628
+ continue
629
+ if not color_overrides:
630
+ continue
631
+ try:
632
+ colors = ThemeColors.merged(existing.colors, color_overrides)
633
+ except ValueError as exc:
634
+ logger.warning(
635
+ "Built-in theme '%s' color override invalid: %s; skipping",
636
+ name,
637
+ exc,
638
+ )
639
+ continue
640
+ builtins[name] = ThemeEntry(
641
+ label=existing.label,
642
+ dark=existing.dark,
643
+ colors=colors,
644
+ custom=existing.custom,
645
+ )
646
+ continue
647
+
648
+ # --- New custom theme: label required, dark defaults to False (light)
649
+ label = section.get("label")
650
+ if not isinstance(label, str) or not label.strip():
651
+ logger.warning(
652
+ "User theme '%s' missing required 'label' (str); skipping",
653
+ name,
654
+ )
655
+ continue
656
+
657
+ dark = section.get("dark", False)
658
+ if not isinstance(dark, bool):
659
+ logger.warning(
660
+ "User theme '%s': 'dark' must be true or false, got %s (%r);"
661
+ " defaulting to light",
662
+ name,
663
+ type(dark).__name__,
664
+ dark,
665
+ )
666
+ dark = False
667
+
668
+ base = DARK_COLORS if dark else LIGHT_COLORS
669
+ try:
670
+ colors = ThemeColors.merged(base, color_overrides)
671
+ except ValueError as exc:
672
+ logger.warning(
673
+ "User theme '%s' has invalid colors: %s; skipping",
674
+ name,
675
+ exc,
676
+ )
677
+ continue
678
+
679
+ builtins[name] = ThemeEntry(
680
+ label=label,
681
+ dark=dark,
682
+ colors=colors,
683
+ custom=True,
684
+ )
685
+
686
+
687
+ def _build_registry(
688
+ *, config_path: Path | None = None
689
+ ) -> MappingProxyType[str, ThemeEntry]:
690
+ """Build and freeze the theme registry (built-in + user themes).
691
+
692
+ Args:
693
+ config_path: Override for the config file path (testing).
694
+
695
+ Returns:
696
+ Read-only mapping of theme names to `ThemeEntry` instances.
697
+ """
698
+ r = _builtin_themes()
699
+ _load_user_themes(r, config_path=config_path)
700
+ return MappingProxyType(r)
701
+
702
+
703
+ @functools.cache
704
+ def get_registry() -> MappingProxyType[str, ThemeEntry]:
705
+ """Return the read-only theme registry, building it on first access.
706
+
707
+ Lazy so that `theme.py` can be imported on the `deepagents --help` cold
708
+ path without pulling in `textual.theme.BUILTIN_THEMES` (which transitively
709
+ imports Textual, ~470ms). Inside a Textual app, the build is microseconds
710
+ because Textual is already loaded; callers like `_register_custom_themes()`
711
+ iterate the result during `App.__init__`, which warms the cache before any
712
+ user-facing surface (e.g. the theme picker) reads it.
713
+ """
714
+ return _build_registry()
715
+
716
+
717
+ DEFAULT_THEME = "langchain"
718
+ """Theme name used when no preference is saved."""
719
+
720
+
721
+ def reload_registry() -> MappingProxyType[str, ThemeEntry]:
722
+ """Rebuild the theme registry from disk.
723
+
724
+ Re-reads `~/.deepagents/config.toml` for user-defined themes so that
725
+ `/reload` can pick up config changes without restarting the app.
726
+
727
+ Returns:
728
+ The new frozen registry.
729
+ """
730
+ get_registry.cache_clear()
731
+ _builtin_names.cache_clear()
732
+ _textual_colors_cache.clear()
733
+ return get_registry()
734
+
735
+
736
+ def get_css_variable_defaults(
737
+ *, dark: bool = True, colors: ThemeColors | None = None
738
+ ) -> dict[str, str]:
739
+ """Return custom CSS variable defaults for the given mode.
740
+
741
+ Most styling is handled by Textual's built-in CSS variables (`$primary`,
742
+ `$text-muted`, `$error-muted`, etc.). This function only returns
743
+ app-specific semantic variables that have no Textual equivalent.
744
+
745
+ Args:
746
+ dark: Selects `DARK_COLORS` or `LIGHT_COLORS` when `colors` is None.
747
+ colors: Explicit color set to use. Takes precedence over `dark`.
748
+
749
+ Returns:
750
+ Dict of CSS variable names to hex color values.
751
+ """
752
+ c = colors if colors is not None else (DARK_COLORS if dark else LIGHT_COLORS)
753
+ return {
754
+ "mode-bash": c.mode_bash,
755
+ "mode-command": c.mode_command,
756
+ "mode-incognito": c.mode_incognito,
757
+ "skill": c.skill,
758
+ "skill-hover": c.skill_hover,
759
+ "tool": c.tool,
760
+ "tool-hover": c.tool_hover,
761
+ }
762
+
763
+
764
+ def _resolve_app(widget_or_app: object) -> object:
765
+ """Resolve a widget or App to the App instance.
766
+
767
+ Args:
768
+ widget_or_app: Textual `App` or a mounted widget.
769
+
770
+ Returns:
771
+ The resolved App instance.
772
+ """
773
+ return (
774
+ widget_or_app.app # ty: ignore[unresolved-attribute]
775
+ if hasattr(type(widget_or_app), "app")
776
+ else widget_or_app
777
+ )
778
+
779
+
780
+ def _colors_from_textual_theme(app: object) -> ThemeColors:
781
+ """Construct `ThemeColors` from the app's active Textual theme.
782
+
783
+ Reads standard properties (primary, secondary, etc.) from the resolved
784
+ theme so Python-side styling matches CSS. `muted` and `mode_incognito`
785
+ have no Textual equivalent and always source from the dark/light base
786
+ palette. `mode_bash` is derived from the theme's `error` color and
787
+ `mode_command` from `secondary`, both falling back to the base palette
788
+ when non-hex values (e.g. `ansi_blue` in the ANSI theme) are detected.
789
+
790
+ Args:
791
+ app: The Textual App instance.
792
+
793
+ Returns:
794
+ `ThemeColors` derived from the active theme.
795
+ """
796
+ ct = app.current_theme # ty: ignore[unresolved-attribute]
797
+ dark: bool = ct.dark
798
+ base = DARK_COLORS if dark else LIGHT_COLORS
799
+
800
+ def _hex_or(val: str | None, fallback: str) -> str:
801
+ """Return `val` if it is a valid `#RRGGBB` hex color, else `fallback`.
802
+
803
+ Args:
804
+ val: Color string from the active Textual theme (may be `None` or
805
+ a non-hex name like `ansi_blue`).
806
+ fallback: Guaranteed-hex value from our base palette.
807
+
808
+ Returns:
809
+ `val` if it matches `#RRGGBB`, otherwise `fallback`.
810
+ """
811
+ if val is not None and _HEX_RE.match(val):
812
+ return val
813
+ return fallback
814
+
815
+ return ThemeColors(
816
+ primary=_hex_or(ct.primary, base.primary),
817
+ secondary=_hex_or(ct.secondary, base.secondary),
818
+ accent=_hex_or(ct.accent, base.accent),
819
+ panel=_hex_or(ct.panel, base.panel),
820
+ success=_hex_or(ct.success, base.success),
821
+ warning=_hex_or(ct.warning, base.warning),
822
+ error=_hex_or(ct.error, base.error),
823
+ muted=base.muted,
824
+ mode_bash=_hex_or(ct.error, base.mode_bash),
825
+ mode_command=_hex_or(ct.secondary, base.mode_command),
826
+ mode_incognito=base.mode_incognito,
827
+ # No Textual equivalent — always use base palette.
828
+ skill=base.skill,
829
+ skill_hover=base.skill_hover,
830
+ # Derived from Textual's warning color (shared amber hue).
831
+ tool=_hex_or(ct.warning, base.tool),
832
+ # No Textual equivalent — always base palette (may diverge from
833
+ # tool in custom themes that override warning).
834
+ tool_hover=base.tool_hover,
835
+ foreground=_hex_or(ct.foreground, base.foreground),
836
+ background=_hex_or(ct.background, base.background),
837
+ surface=_hex_or(ct.surface, base.surface),
838
+ )
839
+
840
+
841
+ def get_theme_colors(widget_or_app: App | object | None = None) -> ThemeColors:
842
+ """Return the `ThemeColors` for the active Textual theme.
843
+
844
+ For custom themes (LangChain-branded and user-defined), the pre-built
845
+ `ThemeColors` from the registry is returned directly. For Textual built-in
846
+ themes, colors are resolved dynamically from the actual theme properties so
847
+ Python-side styling stays in sync with CSS variables.
848
+
849
+ Textual widget code should call this instead of reading the module-level
850
+ ANSI constants, which are intended for Rich console output only.
851
+
852
+ Args:
853
+ widget_or_app: Textual `App`, a mounted widget, or `None`.
854
+
855
+ Returns:
856
+ `ThemeColors` for the active theme.
857
+ """
858
+ if widget_or_app is None:
859
+ # Fall back to the active Textual app context var when no explicit
860
+ # widget/app is passed (e.g. from @staticmethod helpers).
861
+ try:
862
+ from textual._context import active_app # noqa: PLC2701
863
+
864
+ widget_or_app = active_app.get()
865
+ except (ImportError, LookupError):
866
+ return DARK_COLORS
867
+ app = _resolve_app(widget_or_app)
868
+ entry = get_registry().get(app.theme) # ty: ignore[unresolved-attribute]
869
+ # Custom themes (LC-branded / user-defined) use pre-built colors.
870
+ if entry is not None and entry.custom:
871
+ return entry.colors
872
+ # Built-in or unrecognized themes — derive from the resolved Textual
873
+ # theme so Python styling matches CSS. Cache only registered built-ins,
874
+ # since unregistered runtime themes may reuse a name with different colors.
875
+ try:
876
+ ct = app.current_theme # ty: ignore[unresolved-attribute]
877
+ if entry is None:
878
+ colors = _colors_from_textual_theme(app)
879
+ else:
880
+ key = (app.theme, bool(ct.dark)) # ty: ignore[unresolved-attribute]
881
+ colors = _textual_colors_cache.get(key)
882
+ if colors is None:
883
+ colors = _colors_from_textual_theme(app)
884
+ _textual_colors_cache[key] = colors
885
+ except Exception:
886
+ logger.warning("Could not resolve theme colors dynamically", exc_info=True)
887
+ if entry is not None:
888
+ return entry.colors
889
+ return DARK_COLORS
890
+ else:
891
+ return colors