synapse-cli-agent 0.1.13__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 (131) hide show
  1. synapse/__init__.py +13 -0
  2. synapse/__main__.py +6 -0
  3. synapse/app/__init__.py +1 -0
  4. synapse/app/agent.py +492 -0
  5. synapse/app/agent_md.py +107 -0
  6. synapse/cli.py +750 -0
  7. synapse/commands/__init__.py +1 -0
  8. synapse/commands/compression.py +573 -0
  9. synapse/commands/helpers.py +22 -0
  10. synapse/commands/mcp.py +406 -0
  11. synapse/commands/model.py +173 -0
  12. synapse/commands/result.py +34 -0
  13. synapse/commands/sessions.py +443 -0
  14. synapse/commands/slash_cmds.py +521 -0
  15. synapse/commands/slash_complete.py +816 -0
  16. synapse/commands/theme.py +99 -0
  17. synapse/config.py +27 -0
  18. synapse/content/__init__.py +1 -0
  19. synapse/content/input_history.py +122 -0
  20. synapse/content/multimodal.py +733 -0
  21. synapse/content/prompts.py +249 -0
  22. synapse/content/skills_catalog.py +128 -0
  23. synapse/integrations/__init__.py +1 -0
  24. synapse/integrations/checkpoint_seed.py +281 -0
  25. synapse/integrations/codex_history.py +375 -0
  26. synapse/integrations/codex_import.py +393 -0
  27. synapse/integrations/codex_sessions.py +629 -0
  28. synapse/integrations/describe_image.py +370 -0
  29. synapse/integrations/http_clients.py +199 -0
  30. synapse/integrations/llm_openai_compat.py +90 -0
  31. synapse/integrations/llm_openai_websocket.py +187 -0
  32. synapse/integrations/mcp_client.py +646 -0
  33. synapse/integrations/vision_middleware.py +62 -0
  34. synapse/models/__init__.py +5 -0
  35. synapse/models/config.py +240 -0
  36. synapse/models/helpers.py +206 -0
  37. synapse/models/profile.py +59 -0
  38. synapse/models/registry.py +722 -0
  39. synapse/models_registry.py +7 -0
  40. synapse/observability/__init__.py +1 -0
  41. synapse/observability/startup_trace.py +127 -0
  42. synapse/runtime/__init__.py +1 -0
  43. synapse/runtime/async_runtime.py +176 -0
  44. synapse/runtime/backends.py +458 -0
  45. synapse/runtime/context_compact.py +249 -0
  46. synapse/runtime/execute_capture.py +48 -0
  47. synapse/runtime/fs_permissions.py +79 -0
  48. synapse/runtime/harness.py +57 -0
  49. synapse/runtime/hitl.py +197 -0
  50. synapse/runtime/interaction_ledger.py +82 -0
  51. synapse/runtime/middleware.py +802 -0
  52. synapse/runtime/model_request_compression_middleware.py +745 -0
  53. synapse/runtime/pathing.py +146 -0
  54. synapse/runtime/safety.py +184 -0
  55. synapse/runtime/steer.py +240 -0
  56. synapse/runtime/subagents.py +207 -0
  57. synapse/runtime/tool_ignore.py +221 -0
  58. synapse/runtime/tool_output_eval.py +118 -0
  59. synapse/runtime/tool_output_middleware.py +585 -0
  60. synapse/runtime/tool_output_usage_middleware.py +60 -0
  61. synapse/sessions/__init__.py +31 -0
  62. synapse/sessions/cancel_repair.py +208 -0
  63. synapse/sessions/session_recap.py +174 -0
  64. synapse/sessions/store.py +695 -0
  65. synapse/sessions/transcript.py +754 -0
  66. synapse/settings/__init__.py +5 -0
  67. synapse/settings/config_paths.py +184 -0
  68. synapse/settings/schema.py +464 -0
  69. synapse/tool_output/__init__.py +59 -0
  70. synapse/tool_output/detection.py +170 -0
  71. synapse/tool_output/metrics.py +32 -0
  72. synapse/tool_output/models.py +173 -0
  73. synapse/tool_output/pipeline.py +330 -0
  74. synapse/tool_output/repository.py +721 -0
  75. synapse/tool_output/transformers.py +648 -0
  76. synapse/tools/__init__.py +5 -0
  77. synapse/tools/session_tools.py +204 -0
  78. synapse/ui/__init__.py +10 -0
  79. synapse/ui/bottombar/__init__.py +73 -0
  80. synapse/ui/bottombar/components/__init__.py +143 -0
  81. synapse/ui/bottombar/components/key_hints.py +30 -0
  82. synapse/ui/bottombar/components/mcp.py +64 -0
  83. synapse/ui/bottombar/components/mode.py +24 -0
  84. synapse/ui/bottombar/components/model.py +28 -0
  85. synapse/ui/bottombar/components/thread.py +29 -0
  86. synapse/ui/bottombar/context.py +36 -0
  87. synapse/ui/bottombar/core.py +74 -0
  88. synapse/ui/dialogs/__init__.py +25 -0
  89. synapse/ui/dialogs/base.py +362 -0
  90. synapse/ui/dialogs/codex_session_list.py +84 -0
  91. synapse/ui/dialogs/compression_diagnostics.py +210 -0
  92. synapse/ui/dialogs/git_explore.py +702 -0
  93. synapse/ui/dialogs/mcp_panel.py +407 -0
  94. synapse/ui/dialogs/model_picker.py +128 -0
  95. synapse/ui/dialogs/safety_panel.py +63 -0
  96. synapse/ui/dialogs/session_list.py +98 -0
  97. synapse/ui/dialogs/theme_designer.py +863 -0
  98. synapse/ui/dialogs/theme_picker.py +113 -0
  99. synapse/ui/git_explore/__init__.py +31 -0
  100. synapse/ui/git_explore/engine.py +82 -0
  101. synapse/ui/git_explore/provider.py +242 -0
  102. synapse/ui/git_explore/unified.py +85 -0
  103. synapse/ui/rendering.py +350 -0
  104. synapse/ui/sink.py +70 -0
  105. synapse/ui/steer_widget.py +367 -0
  106. synapse/ui/stream.py +1207 -0
  107. synapse/ui/stream_events.py +421 -0
  108. synapse/ui/stream_runtime.py +252 -0
  109. synapse/ui/theme.py +1154 -0
  110. synapse/ui/timeline.py +621 -0
  111. synapse/ui/topbar/__init__.py +97 -0
  112. synapse/ui/topbar/components/__init__.py +150 -0
  113. synapse/ui/topbar/components/branch.py +41 -0
  114. synapse/ui/topbar/components/title.py +24 -0
  115. synapse/ui/topbar/components/tool_output.py +24 -0
  116. synapse/ui/topbar/components/usage.py +24 -0
  117. synapse/ui/topbar/components/workspace.py +32 -0
  118. synapse/ui/topbar/context.py +32 -0
  119. synapse/ui/topbar/core.py +979 -0
  120. synapse/ui/topbar/git_changes_popover.py +178 -0
  121. synapse/ui/topbar/git_chrome.py +475 -0
  122. synapse/ui/topbar/tool_output_popover.py +84 -0
  123. synapse/ui/topbar/widget.py +474 -0
  124. synapse/ui/tui.py +5717 -0
  125. synapse/ui/turn_rail.py +71 -0
  126. synapse/ui/user_turn.py +83 -0
  127. synapse/ui/welcome.py +261 -0
  128. synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
  129. synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
  130. synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
  131. synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
synapse/ui/theme.py ADDED
@@ -0,0 +1,1154 @@
1
+ """UI theme registry: built-ins, layered themes.json, runtime switch.
2
+
3
+ Config surfaces:
4
+
5
+ - ``settings.json`` key ``theme`` (active name), loaded via ``Settings.theme``
6
+ - ``.coding-agent/themes.json`` (user → project layers)::
7
+
8
+ {
9
+ "themes": {
10
+ "my-dark": {
11
+ "extends": "cursor-dark",
12
+ "label": "My Dark",
13
+ "bg": "#0d1117",
14
+ "user": "#58a6ff",
15
+ "top_pad_x": 1,
16
+ "top_gap": 3
17
+ }
18
+ }
19
+ }
20
+
21
+ Topbar layout metrics (CSS / packing) and optional region bands:
22
+
23
+ - ``top`` — whole-row ``#topbar`` background via ``$theme-top``
24
+ - ``top_pad_x`` — horizontal CSS padding cells (default 1; ``$theme-top-pad-x``)
25
+ - ``top_gap`` — cells between left/center/right slots (default 3)
26
+ - ``top_left`` / ``top_center`` / ``top_right`` — optional per-region backgrounds
27
+ - omit or empty region colors → no bands (default for every built-in theme)
28
+
29
+ Built-in ``ansi`` inherits the terminal palette with transparent surfaces
30
+ (terminal wallpaper / acrylic). Aliases: inherit, terminal, auto.
31
+
32
+ Runtime:
33
+
34
+ - ``bootstrap_theme(name, workspace=...)`` at app start
35
+ - ``set_theme(name, persist=True)`` from ``/theme``
36
+ - ``get_theme()`` for Rich/Textual paint paths
37
+ - ``apply_textual_theme(app)`` switches Textual ``App.theme`` (needed for
38
+ transparent / ANSI surfaces; solid palettes use dark/light shells)
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ from collections.abc import Callable
45
+ from dataclasses import dataclass, fields, replace
46
+ from pathlib import Path
47
+ from typing import Any
48
+
49
+ from synapse.settings.config_paths import (
50
+ SETTINGS_FILENAME,
51
+ existing_files,
52
+ layered_config_dirs,
53
+ load_json_object,
54
+ load_layered_json,
55
+ project_config_dir,
56
+ user_config_dir,
57
+ )
58
+
59
+ THEMES_FILENAME = "themes.json"
60
+ DEFAULT_THEME_NAME = "cursor-dark"
61
+
62
+ # Palette keys that may appear in themes.json entries (besides name/label/extends).
63
+ _PALETTE_KEYS = frozenset(
64
+ {
65
+ "fg",
66
+ "dim",
67
+ "muted",
68
+ "green",
69
+ "orange",
70
+ "bar",
71
+ "bg",
72
+ "top",
73
+ "top_left",
74
+ "top_center",
75
+ "top_right",
76
+ "top_pad_x",
77
+ "top_gap",
78
+ "user",
79
+ "border",
80
+ "border_focus",
81
+ "error",
82
+ "code_theme",
83
+ "rich_user",
84
+ "rich_info_border",
85
+ "rich_ok_border",
86
+ "rich_error",
87
+ "rich_activity",
88
+ "ansi",
89
+ "css_fg",
90
+ "css_dim",
91
+ "css_muted",
92
+ "css_green",
93
+ "css_orange",
94
+ "css_bar",
95
+ "css_user",
96
+ "css_error",
97
+ "css_border",
98
+ "css_border_focus",
99
+ "prompt_border",
100
+ }
101
+ )
102
+
103
+ # Integer layout keys in themes.json (not colors).
104
+ _INT_PALETTE_KEYS = frozenset({"top_pad_x", "top_gap"})
105
+
106
+ # Textual 支持的 border 类型。
107
+ _VALID_PROMPT_BORDER_STYLES: frozenset[str] = frozenset({
108
+ "ascii",
109
+ "blank",
110
+ "block",
111
+ "dashed",
112
+ "double",
113
+ "heavy",
114
+ "hidden",
115
+ "hkey",
116
+ "inner",
117
+ "none",
118
+ "outer",
119
+ "panel",
120
+ "round",
121
+ "solid",
122
+ "tab",
123
+ "tall",
124
+ "thick",
125
+ "vkey",
126
+ "wide",
127
+ })
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class Theme:
132
+ """One complete UI palette (TUI CSS + Rich text styles).
133
+
134
+ For terminal-inherit themes (``ansi``), Rich Text styles use names like
135
+ ``default`` / ``bright_black``, while Textual CSS needs ``ansi_default`` /
136
+ ``transparent``. Optional ``css_*`` fields override the CSS side only.
137
+ """
138
+
139
+ name: str
140
+ label: str
141
+ fg: str
142
+ dim: str
143
+ muted: str
144
+ green: str
145
+ orange: str
146
+ bar: str
147
+ bg: str
148
+ top: str
149
+ user: str
150
+ border: str
151
+ border_focus: str
152
+ error: str = "#f28b82"
153
+ code_theme: str = "monokai"
154
+ rich_user: str = "bold cyan"
155
+ rich_info_border: str = "blue"
156
+ rich_ok_border: str = "green"
157
+ rich_error: str = "bold red"
158
+ rich_activity: str = "cyan"
159
+ # When True, surfaces stay transparent and Textual uses native ANSI colors.
160
+ ansi: bool = False
161
+ # Topbar left/center/right band backgrounds (empty = derived defaults).
162
+ # ``none`` / ``off`` / ``transparent`` suppresses that band.
163
+ top_left: str = ""
164
+ top_center: str = ""
165
+ top_right: str = ""
166
+ # Outer horizontal padding of #topbar (cells). CSS: $theme-top-pad-x.
167
+ top_pad_x: int = 1
168
+ # Gap cells between left/center/right slots (packing gap_after).
169
+ top_gap: int = 3
170
+ # CSS-only overrides (empty -> use the matching Rich field above).
171
+ css_fg: str = ""
172
+ css_dim: str = ""
173
+ css_muted: str = ""
174
+ css_green: str = ""
175
+ css_orange: str = ""
176
+ css_bar: str = ""
177
+ css_user: str = ""
178
+ css_error: str = ""
179
+ css_border: str = ""
180
+ css_border_focus: str = ""
181
+ # Border style for #prompt input: tall, heavy, dashed, double, round, solid.
182
+ prompt_border: str = "tall"
183
+
184
+ def css_variables(self) -> dict[str, str]:
185
+ """Textual stylesheet variables (names without leading ``$``)."""
186
+ pad = max(0, int(self.top_pad_x or 0))
187
+ border_style = self.prompt_border
188
+ if border_style not in _VALID_PROMPT_BORDER_STYLES:
189
+ border_style = "tall"
190
+ return {
191
+ "theme-fg": self.css_fg or self.fg,
192
+ "theme-dim": self.css_dim or self.dim,
193
+ "theme-muted": self.css_muted or self.muted,
194
+ "theme-green": self.css_green or self.green,
195
+ "theme-orange": self.css_orange or self.orange,
196
+ "theme-bar": self.css_bar or self.bar,
197
+ "theme-bg": self.bg,
198
+ "theme-top": self.top,
199
+ "theme-user": self.css_user or self.user,
200
+ "theme-border": self.css_border or self.border,
201
+ "theme-border-focus": self.css_border_focus or self.border_focus,
202
+ "theme-error": self.css_error or self.error,
203
+ "theme-top-pad-x": str(pad),
204
+ "theme-prompt-border-style": border_style,
205
+ }
206
+
207
+ @property
208
+ def is_terminal_inherit(self) -> bool:
209
+ """True when chrome should inherit terminal bg (transparent / ANSI)."""
210
+ if self.ansi:
211
+ return True
212
+ bg = (self.bg or "").strip().casefold()
213
+ return bg in {"transparent", "ansi_default", "default"}
214
+
215
+ def topbar_region_bands(self) -> dict[str, tuple[str, str]]:
216
+ """Resolved left/center/right ``(fg, bg)`` for optional region bands.
217
+
218
+ Empty ``bg`` means no band paint (widget CSS ``$theme-top`` shows).
219
+ Built-ins leave ``top_*`` empty so defaults paint no blocks; set
220
+ ``top_left`` / ``top_center`` / ``top_right`` in themes.json to enable.
221
+ """
222
+
223
+ def resolve(explicit: str) -> str:
224
+ key = (explicit or "").strip()
225
+ if not key:
226
+ return ""
227
+ low = key.casefold()
228
+ if low in {"none", "off", "false", "0", "transparent", "inherit", "default"}:
229
+ return ""
230
+ return key
231
+
232
+ left_bg = resolve(self.top_left)
233
+ center_bg = resolve(self.top_center)
234
+ right_bg = resolve(self.top_right)
235
+ left_fg = self.fg or ("default" if self.is_terminal_inherit else "")
236
+ center_fg = self.fg or ("default" if self.is_terminal_inherit else "")
237
+ right_fg = self.dim or ("bright_black" if self.is_terminal_inherit else "")
238
+
239
+ return {
240
+ "left": (left_fg, left_bg),
241
+ "center": (center_fg, center_bg),
242
+ "right": (right_fg, right_bg),
243
+ }
244
+
245
+
246
+ def _t(
247
+ name: str,
248
+ label: str,
249
+ *,
250
+ fg: str,
251
+ dim: str,
252
+ muted: str,
253
+ green: str,
254
+ orange: str,
255
+ bar: str,
256
+ bg: str,
257
+ top: str,
258
+ user: str,
259
+ border: str,
260
+ border_focus: str,
261
+ error: str = "#f28b82",
262
+ code_theme: str = "monokai",
263
+ rich_user: str = "bold cyan",
264
+ rich_info_border: str = "blue",
265
+ rich_ok_border: str = "green",
266
+ rich_error: str = "bold red",
267
+ rich_activity: str = "cyan",
268
+ ansi: bool = False,
269
+ top_left: str = "",
270
+ top_center: str = "",
271
+ top_right: str = "",
272
+ top_pad_x: int = 1,
273
+ top_gap: int = 3,
274
+ css_fg: str = "",
275
+ css_dim: str = "",
276
+ css_muted: str = "",
277
+ css_green: str = "",
278
+ css_orange: str = "",
279
+ css_bar: str = "",
280
+ css_user: str = "",
281
+ css_error: str = "",
282
+ css_border: str = "",
283
+ css_border_focus: str = "",
284
+ prompt_border: str = "tall",
285
+ ) -> Theme:
286
+ return Theme(
287
+ name=name,
288
+ label=label,
289
+ fg=fg,
290
+ dim=dim,
291
+ muted=muted,
292
+ green=green,
293
+ orange=orange,
294
+ bar=bar,
295
+ bg=bg,
296
+ top=top,
297
+ user=user,
298
+ border=border,
299
+ border_focus=border_focus,
300
+ error=error,
301
+ code_theme=code_theme,
302
+ rich_user=rich_user,
303
+ rich_info_border=rich_info_border,
304
+ rich_ok_border=rich_ok_border,
305
+ rich_error=rich_error,
306
+ rich_activity=rich_activity,
307
+ ansi=ansi,
308
+ top_left=top_left,
309
+ top_center=top_center,
310
+ top_right=top_right,
311
+ top_pad_x=max(0, int(top_pad_x or 0)),
312
+ top_gap=max(0, int(top_gap or 0)),
313
+ css_fg=css_fg,
314
+ css_dim=css_dim,
315
+ css_muted=css_muted,
316
+ css_green=css_green,
317
+ css_orange=css_orange,
318
+ css_bar=css_bar,
319
+ css_user=css_user,
320
+ css_error=css_error,
321
+ css_border=css_border,
322
+ css_border_focus=css_border_focus,
323
+ prompt_border=prompt_border,
324
+ )
325
+
326
+
327
+ # Built-in classic palettes (dark + light + terminal-inherit ansi).
328
+ BUILTIN_THEMES: dict[str, Theme] = {
329
+ # Inherit terminal colors; transparent surfaces (acrylic / wallpaper).
330
+ # Rich Text: default / bright_black / green (no ansi_ prefix).
331
+ # CSS: transparent + ansi_* tokens for Textual native ANSI path.
332
+ "ansi": _t(
333
+ "ansi",
334
+ "Terminal (transparent)",
335
+ fg="default",
336
+ dim="bright_black",
337
+ muted="bright_black",
338
+ green="green",
339
+ orange="yellow",
340
+ bar="default",
341
+ bg="transparent",
342
+ top="transparent",
343
+ user="cyan",
344
+ border="bright_black",
345
+ border_focus="cyan",
346
+ error="red",
347
+ code_theme="ansi_dark",
348
+ rich_user="bold cyan",
349
+ rich_info_border="cyan",
350
+ rich_ok_border="green",
351
+ rich_error="bold red",
352
+ rich_activity="cyan",
353
+ ansi=True,
354
+ css_fg="ansi_default",
355
+ css_dim="ansi_bright_black",
356
+ css_muted="ansi_bright_black",
357
+ css_green="ansi_green",
358
+ css_orange="ansi_yellow",
359
+ css_bar="transparent",
360
+ css_user="ansi_cyan",
361
+ css_error="ansi_red",
362
+ css_border="ansi_bright_black",
363
+ css_border_focus="ansi_cyan",
364
+ ),
365
+ "cursor-dark": _t(
366
+ "cursor-dark",
367
+ "Cursor Dark",
368
+ fg="#e8eaed",
369
+ dim="#9aa0a6",
370
+ muted="#5f6368",
371
+ green="#81c995",
372
+ orange="#f4b183",
373
+ bar="#2b2d31",
374
+ bg="#1a1b1e",
375
+ top="#121316",
376
+ user="#8ab4f8",
377
+ border="#3c4043",
378
+ border_focus="#5f6368",
379
+ code_theme="monokai",
380
+ ),
381
+ "github-dark": _t(
382
+ "github-dark",
383
+ "GitHub Dark",
384
+ fg="#e6edf3",
385
+ dim="#8b949e",
386
+ muted="#6e7681",
387
+ green="#3fb950",
388
+ orange="#d29922",
389
+ bar="#21262d",
390
+ bg="#0d1117",
391
+ top="#010409",
392
+ user="#58a6ff",
393
+ border="#30363d",
394
+ border_focus="#8b949e",
395
+ error="#f85149",
396
+ code_theme="github-dark",
397
+ rich_user="bold #58a6ff",
398
+ rich_info_border="#58a6ff",
399
+ rich_ok_border="#3fb950",
400
+ rich_activity="#58a6ff",
401
+ ),
402
+ "dracula": _t(
403
+ "dracula",
404
+ "Dracula",
405
+ fg="#f8f8f2",
406
+ dim="#bd93f9",
407
+ muted="#6272a4",
408
+ green="#50fa7b",
409
+ orange="#ffb86c",
410
+ bar="#44475a",
411
+ bg="#282a36",
412
+ top="#21222c",
413
+ user="#8be9fd",
414
+ border="#6272a4",
415
+ border_focus="#bd93f9",
416
+ error="#ff5555",
417
+ code_theme="dracula",
418
+ rich_user="bold #8be9fd",
419
+ rich_info_border="#bd93f9",
420
+ rich_ok_border="#50fa7b",
421
+ rich_activity="#ff79c6",
422
+ ),
423
+ "nord": _t(
424
+ "nord",
425
+ "Nord",
426
+ fg="#eceff4",
427
+ dim="#d8dee9",
428
+ muted="#4c566a",
429
+ green="#a3be8c",
430
+ orange="#d08770",
431
+ bar="#3b4252",
432
+ bg="#2e3440",
433
+ top="#242933",
434
+ user="#88c0d0",
435
+ border="#4c566a",
436
+ border_focus="#81a1c1",
437
+ error="#bf616a",
438
+ code_theme="nord",
439
+ rich_user="bold #88c0d0",
440
+ rich_info_border="#81a1c1",
441
+ rich_ok_border="#a3be8c",
442
+ rich_activity="#88c0d0",
443
+ ),
444
+ "solarized-dark": _t(
445
+ "solarized-dark",
446
+ "Solarized Dark",
447
+ fg="#93a1a1",
448
+ dim="#839496",
449
+ muted="#586e75",
450
+ green="#859900",
451
+ orange="#cb4b16",
452
+ bar="#073642",
453
+ bg="#002b36",
454
+ top="#001f27",
455
+ user="#268bd2",
456
+ border="#586e75",
457
+ border_focus="#839496",
458
+ error="#dc322f",
459
+ code_theme="solarized-dark",
460
+ rich_user="bold #268bd2",
461
+ rich_info_border="#268bd2",
462
+ rich_ok_border="#859900",
463
+ rich_activity="#2aa198",
464
+ ),
465
+ "solarized-light": _t(
466
+ "solarized-light",
467
+ "Solarized Light",
468
+ fg="#465c63",
469
+ dim="#526b73",
470
+ muted="#657b83",
471
+ green="#859900",
472
+ orange="#cb4b16",
473
+ bar="#eee8d5",
474
+ bg="#fdf6e3",
475
+ top="#eee8d5",
476
+ user="#268bd2",
477
+ border="#93a1a1",
478
+ border_focus="#657b83",
479
+ error="#dc322f",
480
+ code_theme="solarized-light",
481
+ rich_user="bold #268bd2",
482
+ rich_info_border="#268bd2",
483
+ rich_ok_border="#859900",
484
+ rich_activity="#2aa198",
485
+ ),
486
+ "catppuccin-mocha": _t(
487
+ "catppuccin-mocha",
488
+ "Catppuccin Mocha",
489
+ fg="#cdd6f4",
490
+ dim="#a6adc8",
491
+ muted="#6c7086",
492
+ green="#a6e3a1",
493
+ orange="#fab387",
494
+ bar="#313244",
495
+ bg="#1e1e2e",
496
+ top="#181825",
497
+ user="#89b4fa",
498
+ border="#45475a",
499
+ border_focus="#89b4fa",
500
+ error="#f38ba8",
501
+ code_theme="monokai",
502
+ rich_user="bold #89b4fa",
503
+ rich_info_border="#89b4fa",
504
+ rich_ok_border="#a6e3a1",
505
+ rich_activity="#cba6f7",
506
+ ),
507
+ "one-dark": _t(
508
+ "one-dark",
509
+ "One Dark",
510
+ fg="#abb2bf",
511
+ dim="#828997",
512
+ muted="#5c6370",
513
+ green="#98c379",
514
+ orange="#d19a66",
515
+ bar="#2c313c",
516
+ bg="#282c34",
517
+ top="#21252b",
518
+ user="#61afef",
519
+ border="#3e4451",
520
+ border_focus="#61afef",
521
+ error="#e06c75",
522
+ code_theme="one-dark",
523
+ rich_user="bold #61afef",
524
+ rich_info_border="#61afef",
525
+ rich_ok_border="#98c379",
526
+ rich_activity="#c678dd",
527
+ ),
528
+ "github-light": _t(
529
+ "github-light",
530
+ "GitHub Light",
531
+ fg="#1f2328",
532
+ dim="#656d76",
533
+ muted="#6b7280",
534
+ green="#1a7f37",
535
+ orange="#9a6700",
536
+ bar="#d8dee4",
537
+ bg="#f6f8fa",
538
+ top="#eaeef2",
539
+ user="#0969da",
540
+ border="#b8c2cc",
541
+ border_focus="#0969da",
542
+ error="#cf222e",
543
+ code_theme="default",
544
+ rich_user="bold #0969da",
545
+ rich_info_border="#0969da",
546
+ rich_ok_border="#1a7f37",
547
+ rich_activity="#8250df",
548
+ ),
549
+ "one-light": _t(
550
+ "one-light",
551
+ "One Light",
552
+ fg="#383a42",
553
+ dim="#5d616c",
554
+ muted="#737680",
555
+ green="#50a14f",
556
+ orange="#c18401",
557
+ bar="#dedfe2",
558
+ bg="#f5f5f6",
559
+ top="#e8e8ea",
560
+ user="#4078f2",
561
+ border="#b9bbc1",
562
+ border_focus="#4078f2",
563
+ error="#e45649",
564
+ code_theme="default",
565
+ rich_user="bold #4078f2",
566
+ rich_info_border="#4078f2",
567
+ rich_ok_border="#50a14f",
568
+ rich_activity="#a626a4",
569
+ ),
570
+ "gruvbox-light": _t(
571
+ "gruvbox-light",
572
+ "Gruvbox Light",
573
+ fg="#3c3836",
574
+ dim="#6f6258",
575
+ muted="#7c6f64",
576
+ green="#79740e",
577
+ orange="#af3a03",
578
+ bar="#ebdbb2",
579
+ bg="#fbf1c7",
580
+ top="#f2e5bc",
581
+ user="#076678",
582
+ border="#d5c4a1",
583
+ border_focus="#076678",
584
+ error="#cc241d",
585
+ code_theme="default",
586
+ rich_user="bold #076678",
587
+ rich_info_border="#076678",
588
+ rich_ok_border="#79740e",
589
+ rich_activity="#8f3f71",
590
+ ),
591
+ "catppuccin-latte": _t(
592
+ "catppuccin-latte",
593
+ "Catppuccin Latte",
594
+ fg="#4c4f69",
595
+ dim="#5e6178",
596
+ muted="#73778a",
597
+ green="#40a02b",
598
+ orange="#fe640b",
599
+ bar="#e6e9ef",
600
+ bg="#eff1f5",
601
+ top="#dce0e8",
602
+ user="#1e66f5",
603
+ border="#ccd0da",
604
+ border_focus="#1e66f5",
605
+ error="#d20f39",
606
+ code_theme="default",
607
+ rich_user="bold #1e66f5",
608
+ rich_info_border="#1e66f5",
609
+ rich_ok_border="#40a02b",
610
+ rich_activity="#8839ef",
611
+ ),
612
+ "tokyo-night-light": _t(
613
+ "tokyo-night-light",
614
+ "Tokyo Night Light",
615
+ fg="#343b58",
616
+ dim="#4c5168",
617
+ muted="#666d92",
618
+ green="#485e30",
619
+ orange="#965027",
620
+ bar="#c8d3f5",
621
+ bg="#d5d6db",
622
+ top="#c0c2ce",
623
+ user="#2e7de9",
624
+ border="#a8aecb",
625
+ border_focus="#2e7de9",
626
+ error="#8c4351",
627
+ code_theme="default",
628
+ rich_user="bold #2e7de9",
629
+ rich_info_border="#2e7de9",
630
+ rich_ok_border="#485e30",
631
+ rich_activity="#9854f1",
632
+ ),
633
+ "ayu-light": _t(
634
+ "ayu-light",
635
+ "Ayu Light",
636
+ fg="#575f66",
637
+ dim="#5d6875",
638
+ muted="#687582",
639
+ green="#6cbf43",
640
+ orange="#f29718",
641
+ bar="#e3e7eb",
642
+ bg="#f6f8fa",
643
+ top="#e7ebef",
644
+ user="#399ee6",
645
+ border="#c5cbd2",
646
+ border_focus="#399ee6",
647
+ error="#f07178",
648
+ code_theme="default",
649
+ rich_user="bold #399ee6",
650
+ rich_info_border="#399ee6",
651
+ rich_ok_border="#6cbf43",
652
+ rich_activity="#a37acc",
653
+ ),
654
+ "nord-light": _t(
655
+ "nord-light",
656
+ "Nord Light",
657
+ fg="#3b4252",
658
+ dim="#4c566a",
659
+ muted="#60738d",
660
+ green="#719839",
661
+ orange="#c98245",
662
+ bar="#e5e9f0",
663
+ bg="#eceff4",
664
+ top="#d8dee9",
665
+ user="#5e81ac",
666
+ border="#d8dee9",
667
+ border_focus="#5e81ac",
668
+ error="#bf616a",
669
+ code_theme="default",
670
+ rich_user="bold #5e81ac",
671
+ rich_info_border="#5e81ac",
672
+ rich_ok_border="#719839",
673
+ rich_activity="#b48ead",
674
+ ),
675
+ }
676
+
677
+
678
+ _active: Theme = BUILTIN_THEMES[DEFAULT_THEME_NAME]
679
+ _custom: dict[str, Theme] = {}
680
+ _listeners: list[Callable[[Theme], None]] = []
681
+ _loaded_workspace: str | None = None
682
+
683
+
684
+ def on_theme_change(callback: Callable[[Theme], None]) -> None:
685
+ """Register a listener invoked after the active theme changes."""
686
+ if callback not in _listeners:
687
+ _listeners.append(callback)
688
+
689
+
690
+ def get_theme() -> Theme:
691
+ return _active
692
+
693
+
694
+ def builtin_theme_names() -> list[str]:
695
+ return list(BUILTIN_THEMES.keys())
696
+
697
+
698
+ def list_theme_names() -> list[str]:
699
+ """Built-ins first (stable order), then custom names sorted."""
700
+ names = list(BUILTIN_THEMES.keys())
701
+ extras = sorted(n for n in _custom if n not in BUILTIN_THEMES)
702
+ return names + extras
703
+
704
+
705
+ def list_themes() -> list[Theme]:
706
+ return [get_theme_by_name(n) for n in list_theme_names()]
707
+
708
+
709
+ def get_theme_by_name(name: str) -> Theme:
710
+ key = (name or "").strip()
711
+ if key in _custom:
712
+ return _custom[key]
713
+ if key in BUILTIN_THEMES:
714
+ return BUILTIN_THEMES[key]
715
+ raise KeyError(f"unknown theme: {name!r}")
716
+
717
+
718
+ def _normalize_color(value: object) -> str | None:
719
+ if value is None:
720
+ return None
721
+ text = str(value).strip()
722
+ if not text:
723
+ return None
724
+ return text
725
+
726
+
727
+ def _theme_from_dict(
728
+ name: str,
729
+ data: dict[str, Any],
730
+ *,
731
+ catalog: dict[str, Theme],
732
+ stack: list[str] | None = None,
733
+ ) -> Theme:
734
+ """Build a Theme from a config dict, supporting ``extends``."""
735
+ stack = list(stack or [])
736
+ if name in stack:
737
+ raise ValueError(f"theme extends cycle: {' -> '.join([*stack, name])}")
738
+ stack.append(name)
739
+
740
+ extends = str(data.get("extends") or data.get("base") or "").strip()
741
+ if extends:
742
+ if extends in catalog:
743
+ base = catalog[extends]
744
+ elif extends in BUILTIN_THEMES:
745
+ base = BUILTIN_THEMES[extends]
746
+ elif extends in _custom:
747
+ base = _custom[extends]
748
+ else:
749
+ # Resolve peer custom not yet fully built.
750
+ raise KeyError(f"theme {name!r} extends unknown base {extends!r}")
751
+ else:
752
+ base = BUILTIN_THEMES[DEFAULT_THEME_NAME]
753
+
754
+ label = str(data.get("label") or data.get("title") or name).strip() or name
755
+ updates: dict[str, Any] = {"name": name, "label": label}
756
+ for key in _PALETTE_KEYS:
757
+ if key not in data:
758
+ continue
759
+ raw = data[key]
760
+ if key == "code_theme":
761
+ val = str(raw).strip() if raw is not None else ""
762
+ if val:
763
+ updates[key] = val
764
+ continue
765
+ if key == "prompt_border":
766
+ val = str(raw).strip() if raw is not None else ""
767
+ if val and val in _VALID_PROMPT_BORDER_STYLES:
768
+ updates[key] = val
769
+ continue
770
+ if key == "ansi":
771
+ if isinstance(raw, bool):
772
+ updates[key] = raw
773
+ elif raw is not None:
774
+ updates[key] = str(raw).strip().casefold() in {
775
+ "1",
776
+ "true",
777
+ "yes",
778
+ "on",
779
+ }
780
+ continue
781
+ if key in _INT_PALETTE_KEYS:
782
+ try:
783
+ updates[key] = max(0, int(raw))
784
+ except (TypeError, ValueError):
785
+ pass
786
+ continue
787
+ color = _normalize_color(raw)
788
+ if color is not None:
789
+ updates[key] = color
790
+ return replace(base, **updates)
791
+
792
+
793
+ def _parse_themes_blob(blob: dict[str, Any]) -> dict[str, Theme]:
794
+ """Parse a themes.json root object into name -> Theme."""
795
+ section = blob.get("themes")
796
+ if section is None:
797
+ # Allow flat map of name -> palette (without wrapper).
798
+ section = {
799
+ k: v
800
+ for k, v in blob.items()
801
+ if k not in {"active", "default", "theme"} and isinstance(v, dict)
802
+ }
803
+ if not isinstance(section, dict):
804
+ return {}
805
+
806
+ # Multi-pass so extends can refer to peers defined in the same file.
807
+ pending: dict[str, dict[str, Any]] = {
808
+ str(k).strip(): v for k, v in section.items() if str(k).strip() and isinstance(v, dict)
809
+ }
810
+ built: dict[str, Theme] = {}
811
+ # Prefer pure built-in extends first.
812
+ guard = 0
813
+ while pending and guard < 32:
814
+ guard += 1
815
+ progress = False
816
+ for name in list(pending.keys()):
817
+ data = pending[name]
818
+ extends = str(data.get("extends") or data.get("base") or "").strip()
819
+ if (
820
+ extends
821
+ and extends not in BUILTIN_THEMES
822
+ and extends not in built
823
+ and extends in pending
824
+ ):
825
+ continue
826
+ try:
827
+ built[name] = _theme_from_dict(name, data, catalog=built)
828
+ except KeyError:
829
+ continue
830
+ del pending[name]
831
+ progress = True
832
+ if not progress:
833
+ break
834
+ # Last attempt: force-build remaining against defaults / partial catalog.
835
+ for name, data in list(pending.items()):
836
+ try:
837
+ built[name] = _theme_from_dict(name, data, catalog=built)
838
+ except Exception: # noqa: BLE001
839
+ continue
840
+ return built
841
+
842
+
843
+ def load_custom_themes(workspace: Path | str | None = None) -> dict[str, Theme]:
844
+ """Load layered ``themes.json`` (user → project). Later layers override."""
845
+ merged, _paths = load_layered_json(THEMES_FILENAME, workspace)
846
+ if not merged:
847
+ return {}
848
+ return _parse_themes_blob(merged)
849
+
850
+
851
+ def reload_theme_catalog(workspace: Path | str | None = None) -> dict[str, Theme]:
852
+ """Refresh the in-memory custom catalog from disk."""
853
+ global _custom, _loaded_workspace
854
+ _custom = load_custom_themes(workspace)
855
+ try:
856
+ _loaded_workspace = str(Path(workspace).resolve()) if workspace is not None else None
857
+ except Exception: # noqa: BLE001
858
+ _loaded_workspace = str(workspace) if workspace is not None else None
859
+ return dict(_custom)
860
+
861
+
862
+ def _notify(theme: Theme) -> None:
863
+ for cb in list(_listeners):
864
+ try:
865
+ cb(theme)
866
+ except Exception: # noqa: BLE001
867
+ continue
868
+
869
+
870
+ def set_active_theme(theme: Theme) -> Theme:
871
+ """Set the process-wide active theme and notify listeners."""
872
+ global _active
873
+ _active = theme
874
+ _notify(theme)
875
+ return theme
876
+
877
+
878
+ # User-facing aliases that map to the terminal-inherit palette.
879
+ _THEME_ALIASES: dict[str, str] = {
880
+ "inherit": "ansi",
881
+ "terminal": "ansi",
882
+ "auto": "ansi",
883
+ "default": "ansi",
884
+ "transparent": "ansi",
885
+ }
886
+
887
+ # Textual App.theme names registered by ensure_textual_themes().
888
+ TEXTUAL_THEME_ANSI = "synapse-ansi"
889
+ TEXTUAL_THEME_DARK = "synapse-dark"
890
+ TEXTUAL_THEME_LIGHT = "synapse-light"
891
+
892
+
893
+ def resolve_theme_name(name: str | None) -> str:
894
+ text = (name or "").strip()
895
+ if not text:
896
+ return DEFAULT_THEME_NAME
897
+ key = text.casefold()
898
+ if key in _THEME_ALIASES:
899
+ return _THEME_ALIASES[key]
900
+ if text in BUILTIN_THEMES or text in _custom:
901
+ return text
902
+ for candidate in list(BUILTIN_THEMES) + list(_custom):
903
+ if candidate.casefold() == key:
904
+ return candidate
905
+ return text
906
+
907
+
908
+ def _is_light_hex(color: str) -> bool:
909
+ """Rough relative-luminance check for solid #rrggbb backgrounds."""
910
+ c = (color or "").strip().lstrip("#")
911
+ if len(c) < 6:
912
+ return False
913
+ try:
914
+ r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
915
+ except ValueError:
916
+ return False
917
+ lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
918
+ return lum > 0.5
919
+
920
+
921
+ def theme_kind(theme: Theme | None = None) -> str:
922
+ """Classify a palette as ``ansi``, ``light``, or ``dark``."""
923
+ t = theme or get_theme()
924
+ if t.is_terminal_inherit:
925
+ return "ansi"
926
+ return "light" if _is_light_hex(t.bg) else "dark"
927
+
928
+
929
+ def textual_themes() -> list[Any]:
930
+ """Build Textual Theme objects for ``App.register_theme``."""
931
+ from textual.theme import Theme as TextualTheme
932
+
933
+ coding_ansi = TextualTheme(
934
+ name=TEXTUAL_THEME_ANSI,
935
+ primary="ansi_cyan",
936
+ secondary="ansi_blue",
937
+ warning="ansi_yellow",
938
+ error="ansi_red",
939
+ success="ansi_green",
940
+ accent="ansi_cyan",
941
+ foreground="ansi_default",
942
+ background="transparent",
943
+ surface="transparent",
944
+ panel="transparent",
945
+ boost="transparent",
946
+ dark=True,
947
+ ansi=True,
948
+ variables={
949
+ # Built-in ansi-dark paints solid black; keep acrylic/wallpaper.
950
+ "ansi-background": "transparent",
951
+ "ansi-foreground": "ansi_default",
952
+ "border-blurred": "ansi_bright_black",
953
+ "input-cursor-background": "ansi_default",
954
+ "input-cursor-foreground": "ansi_default",
955
+ "input-selection-background": "ansi_bright_blue",
956
+ "input-selection-foreground": "ansi_black",
957
+ "footer-background": "transparent",
958
+ },
959
+ )
960
+ coding_dark = TextualTheme(
961
+ name=TEXTUAL_THEME_DARK,
962
+ primary="#89b4fa",
963
+ secondary="#cba6f7",
964
+ warning="#f4b183",
965
+ error="#f38ba8",
966
+ success="#81c995",
967
+ accent="#89b4fa",
968
+ foreground="#e8eaed",
969
+ background="#1a1b1e",
970
+ surface="#1a1b1e",
971
+ panel="#121316",
972
+ boost="#2b2d31",
973
+ dark=True,
974
+ ansi=False,
975
+ )
976
+ coding_light = TextualTheme(
977
+ name=TEXTUAL_THEME_LIGHT,
978
+ primary="#0969da",
979
+ secondary="#8250df",
980
+ warning="#9a6700",
981
+ error="#cf222e",
982
+ success="#1a7f37",
983
+ accent="#0969da",
984
+ foreground="#1f2328",
985
+ background="#f6f8fa",
986
+ surface="#ffffff",
987
+ panel="#ffffff",
988
+ boost="#eaeef2",
989
+ dark=False,
990
+ ansi=False,
991
+ )
992
+ return [coding_ansi, coding_dark, coding_light]
993
+
994
+
995
+ def ensure_textual_themes(app: Any) -> None:
996
+ """Register synapse-* Textual themes on an App instance (idempotent)."""
997
+ for th in textual_themes():
998
+ try:
999
+ app.register_theme(th)
1000
+ except Exception: # noqa: BLE001
1001
+ pass
1002
+
1003
+
1004
+ def apply_textual_theme(app: Any, theme: Theme | None = None) -> str:
1005
+ """Switch ``App.theme`` so Textual surfaces match the active palette.
1006
+
1007
+ Returns the Textual theme name applied (or attempted).
1008
+ """
1009
+ pal = theme or get_theme()
1010
+ kind = theme_kind(pal)
1011
+ if kind == "ansi":
1012
+ name = TEXTUAL_THEME_ANSI
1013
+ elif kind == "light":
1014
+ name = TEXTUAL_THEME_LIGHT
1015
+ else:
1016
+ name = TEXTUAL_THEME_DARK
1017
+ ensure_textual_themes(app)
1018
+ try:
1019
+ app.theme = name
1020
+ return name
1021
+ except Exception: # noqa: BLE001
1022
+ fallback = {
1023
+ TEXTUAL_THEME_ANSI: "ansi-dark",
1024
+ TEXTUAL_THEME_DARK: "textual-dark",
1025
+ TEXTUAL_THEME_LIGHT: "textual-light",
1026
+ }.get(name, "textual-dark")
1027
+ try:
1028
+ app.theme = fallback
1029
+ except Exception: # noqa: BLE001
1030
+ pass
1031
+ return fallback
1032
+
1033
+
1034
+ def set_theme(
1035
+ name: str | None,
1036
+ *,
1037
+ workspace: Path | str | None = None,
1038
+ persist: bool = False,
1039
+ scope: str = "user",
1040
+ reload: bool = True,
1041
+ ) -> Theme:
1042
+ """Activate a theme by name. Optionally persist to settings.json."""
1043
+ if reload:
1044
+ reload_theme_catalog(workspace)
1045
+ key = resolve_theme_name(name)
1046
+ try:
1047
+ theme = get_theme_by_name(key)
1048
+ except KeyError as exc:
1049
+ raise KeyError(
1050
+ f"unknown theme: {key!r}. available: {', '.join(list_theme_names())}"
1051
+ ) from exc
1052
+ set_active_theme(theme)
1053
+ if persist:
1054
+ persist_theme_preference(theme.name, workspace=workspace, scope=scope)
1055
+ return theme
1056
+
1057
+
1058
+ def bootstrap_theme(
1059
+ name: str | None = None,
1060
+ *,
1061
+ workspace: Path | str | None = None,
1062
+ ) -> Theme:
1063
+ """Load customs + activate ``name`` (fallback: default). Never raises on bad name."""
1064
+ reload_theme_catalog(workspace)
1065
+ key = resolve_theme_name(name)
1066
+ try:
1067
+ return set_theme(key, workspace=workspace, persist=False, reload=False)
1068
+ except KeyError:
1069
+ return set_theme(DEFAULT_THEME_NAME, workspace=workspace, persist=False, reload=False)
1070
+
1071
+
1072
+ def themes_config_paths(workspace: Path | str | None = None) -> list[Path]:
1073
+ return existing_files(layered_config_dirs(workspace), THEMES_FILENAME)
1074
+
1075
+
1076
+ def persist_theme_preference(
1077
+ name: str,
1078
+ *,
1079
+ workspace: Path | str | None = None,
1080
+ scope: str = "user",
1081
+ ) -> Path:
1082
+ """Write ``theme`` into user or project ``settings.json`` (merge, keep other keys)."""
1083
+ key = resolve_theme_name(name)
1084
+ if scope == "project":
1085
+ target_dir = project_config_dir(workspace)
1086
+ else:
1087
+ target_dir = user_config_dir()
1088
+ target_dir.mkdir(parents=True, exist_ok=True)
1089
+ path = target_dir / SETTINGS_FILENAME
1090
+ data: dict[str, Any] = {}
1091
+ if path.is_file():
1092
+ try:
1093
+ data = load_json_object(path)
1094
+ except Exception: # noqa: BLE001
1095
+ data = {}
1096
+ data["theme"] = key
1097
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1098
+ return path
1099
+
1100
+
1101
+ def format_theme_list_lines(*, active: str | None = None) -> list[str]:
1102
+ """Human-readable theme catalog for ``/theme``."""
1103
+ current = (active or get_theme().name).strip() or DEFAULT_THEME_NAME
1104
+ lines = [f"theme: {current}", "available:"]
1105
+ for theme in list_themes():
1106
+ mark = "*" if theme.name == current else " "
1107
+ tone = theme_kind(theme)
1108
+ if theme.name in _custom and theme.name not in BUILTIN_THEMES:
1109
+ kind = f"custom/{tone}"
1110
+ elif theme.name in _custom:
1111
+ kind = f"override/{tone}"
1112
+ else:
1113
+ kind = f"built-in/{tone}"
1114
+ lines.append(f" {mark} {theme.name:20} {theme.label} ({kind})")
1115
+ lines.append("usage: /theme <name>")
1116
+ lines.append(" /theme list")
1117
+ lines.append(" /theme ansi|inherit (terminal transparent)")
1118
+ lines.append("config: settings.json theme + optional themes.json")
1119
+ return lines
1120
+
1121
+
1122
+ def theme_field_names() -> list[str]:
1123
+ return [f.name for f in fields(Theme)]
1124
+
1125
+
1126
+ __all__ = [
1127
+ "BUILTIN_THEMES",
1128
+ "DEFAULT_THEME_NAME",
1129
+ "TEXTUAL_THEME_ANSI",
1130
+ "TEXTUAL_THEME_DARK",
1131
+ "TEXTUAL_THEME_LIGHT",
1132
+ "THEMES_FILENAME",
1133
+ "Theme",
1134
+ "apply_textual_theme",
1135
+ "bootstrap_theme",
1136
+ "builtin_theme_names",
1137
+ "ensure_textual_themes",
1138
+ "format_theme_list_lines",
1139
+ "get_theme",
1140
+ "get_theme_by_name",
1141
+ "list_theme_names",
1142
+ "list_themes",
1143
+ "load_custom_themes",
1144
+ "on_theme_change",
1145
+ "persist_theme_preference",
1146
+ "reload_theme_catalog",
1147
+ "resolve_theme_name",
1148
+ "set_active_theme",
1149
+ "set_theme",
1150
+ "textual_themes",
1151
+ "theme_field_names",
1152
+ "theme_kind",
1153
+ "themes_config_paths",
1154
+ ]