deepy-cli 0.1.4__tar.gz → 0.1.5__tar.gz

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 (71) hide show
  1. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/PKG-INFO +25 -7
  2. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/README.md +24 -6
  3. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/pyproject.toml +1 -1
  4. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/cli.py +73 -35
  6. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/config/__init__.py +18 -0
  7. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/config/settings.py +115 -0
  8. deepy_cli-0.1.5/src/deepy/ui/markdown.py +346 -0
  9. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/message_view.py +46 -39
  10. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/prompt_input.py +22 -12
  11. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/slash_commands.py +2 -0
  12. deepy_cli-0.1.5/src/deepy/ui/styles.py +154 -0
  13. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/terminal.py +278 -53
  14. deepy_cli-0.1.5/src/deepy/ui/theme_picker.py +116 -0
  15. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/welcome.py +35 -21
  16. deepy_cli-0.1.4/src/deepy/ui/markdown.py +0 -152
  17. deepy_cli-0.1.4/src/deepy/ui/styles.py +0 -21
  18. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/__main__.py +0 -0
  19. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/__init__.py +0 -0
  20. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  21. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/WebFetch.md +0 -0
  22. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/WebSearch.md +0 -0
  23. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/__init__.py +0 -0
  24. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/bash.md +0 -0
  25. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/edit.md +0 -0
  26. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/modify.md +0 -0
  27. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/read.md +0 -0
  28. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/data/tools/write.md +0 -0
  29. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/errors.py +0 -0
  30. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/__init__.py +0 -0
  31. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/agent.py +0 -0
  32. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/context.py +0 -0
  33. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/events.py +0 -0
  34. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/model_capabilities.py +0 -0
  35. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/provider.py +0 -0
  36. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/replay.py +0 -0
  37. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/runner.py +0 -0
  38. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/llm/thinking.py +0 -0
  39. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/__init__.py +0 -0
  40. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/compact.py +0 -0
  41. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/rules.py +0 -0
  42. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/runtime_context.py +0 -0
  43. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/system.py +0 -0
  44. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/prompts/tool_docs.py +0 -0
  45. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/sessions/__init__.py +0 -0
  46. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/sessions/jsonl.py +0 -0
  47. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/sessions/manager.py +0 -0
  48. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/skills.py +0 -0
  49. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/status.py +0 -0
  50. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/__init__.py +0 -0
  51. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/agents.py +0 -0
  52. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/builtin.py +0 -0
  53. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/file_state.py +0 -0
  54. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/result.py +0 -0
  55. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/tools/shell_utils.py +0 -0
  56. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/__init__.py +0 -0
  57. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/app.py +0 -0
  58. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/ask_user_question.py +0 -0
  59. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/exit_summary.py +0 -0
  60. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/loading_text.py +0 -0
  61. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/prompt_buffer.py +0 -0
  62. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/session_list.py +0 -0
  63. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/session_picker.py +0 -0
  64. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/ui/thinking_state.py +0 -0
  65. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/update_check.py +0 -0
  66. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/usage.py +0 -0
  67. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/utils/__init__.py +0 -0
  68. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/utils/debug_logger.py +0 -0
  69. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/utils/error_logger.py +0 -0
  70. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/utils/json.py +0 -0
  71. {deepy_cli-0.1.4 → deepy_cli-0.1.5}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -71,8 +71,8 @@ context state visible while the agent works.
71
71
  - Session history, `/resume`, `/new`, automatic context tracking, and compacting
72
72
  for long project work.
73
73
  - TOML-only private configuration at `~/.deepy/config.toml`.
74
- - Terminal UI with Markdown rendering, DeepSeek thinking display, per-turn usage,
75
- context window status, and version update checks.
74
+ - Theme-aware terminal UI with Markdown rendering, DeepSeek thinking display,
75
+ per-turn usage, context window status, and version update checks.
76
76
 
77
77
  ## See It Work
78
78
 
@@ -137,11 +137,17 @@ Deepy only uses TOML configuration. JSON config files are intentionally rejected
137
137
 
138
138
  ```toml
139
139
  # ~/.deepy/config.toml
140
+ [model]
140
141
  api_key = "sk-..."
141
- model = "deepseek-v4-pro"
142
+ name = "deepseek-v4-pro"
142
143
  base_url = "https://api.deepseek.com"
143
- context_window_tokens = 1048576
144
- compact_threshold = 0.8
144
+
145
+ [context]
146
+ window_tokens = 1048576
147
+ compact_trigger_ratio = 0.8
148
+
149
+ [ui]
150
+ theme = "auto" # auto, dark, or light
145
151
  ```
146
152
 
147
153
  WebSearch uses Deepy's hosted SearXNG endpoint by default. You can override it
@@ -158,11 +164,21 @@ You can also initialize config non-interactively:
158
164
  deepy config init --api-key sk-... --model deepseek-v4-pro
159
165
  ```
160
166
 
167
+ If your terminal uses a light background and parts of the UI look low contrast,
168
+ set the UI theme explicitly:
169
+
170
+ ```bash
171
+ deepy config theme light
172
+ ```
173
+
161
174
  ## Common Commands
162
175
 
163
176
  ```bash
164
177
  deepy --version
165
178
  deepy config setup
179
+ deepy config reset
180
+ deepy config theme
181
+ deepy config theme light
166
182
  deepy doctor
167
183
  deepy doctor --live --json
168
184
  deepy status
@@ -178,6 +194,8 @@ Inside the interactive terminal:
178
194
  /skills List available skills
179
195
  /new Start a fresh conversation
180
196
  /resume Pick a previous session
197
+ /theme Show or change UI theme
198
+ /reset Delete config and run setup again
181
199
  / Open the command menu
182
200
  Esc Interrupt the current model turn
183
201
  Ctrl+D Press twice to quit
@@ -208,6 +226,6 @@ assets live outside the package directory and are not included in the wheel.
208
226
 
209
227
  ## Release Status
210
228
 
211
- Deepy is preparing its first public `0.1.4` release. The current release path is
229
+ Deepy is preparing its first public `0.1.5` release. The current release path is
212
230
  GitHub + PyPI. Standalone binaries and npm wrappers can be added later, but the
213
231
  primary distribution is the Python CLI.
@@ -43,8 +43,8 @@ context state visible while the agent works.
43
43
  - Session history, `/resume`, `/new`, automatic context tracking, and compacting
44
44
  for long project work.
45
45
  - TOML-only private configuration at `~/.deepy/config.toml`.
46
- - Terminal UI with Markdown rendering, DeepSeek thinking display, per-turn usage,
47
- context window status, and version update checks.
46
+ - Theme-aware terminal UI with Markdown rendering, DeepSeek thinking display,
47
+ per-turn usage, context window status, and version update checks.
48
48
 
49
49
  ## See It Work
50
50
 
@@ -109,11 +109,17 @@ Deepy only uses TOML configuration. JSON config files are intentionally rejected
109
109
 
110
110
  ```toml
111
111
  # ~/.deepy/config.toml
112
+ [model]
112
113
  api_key = "sk-..."
113
- model = "deepseek-v4-pro"
114
+ name = "deepseek-v4-pro"
114
115
  base_url = "https://api.deepseek.com"
115
- context_window_tokens = 1048576
116
- compact_threshold = 0.8
116
+
117
+ [context]
118
+ window_tokens = 1048576
119
+ compact_trigger_ratio = 0.8
120
+
121
+ [ui]
122
+ theme = "auto" # auto, dark, or light
117
123
  ```
118
124
 
119
125
  WebSearch uses Deepy's hosted SearXNG endpoint by default. You can override it
@@ -130,11 +136,21 @@ You can also initialize config non-interactively:
130
136
  deepy config init --api-key sk-... --model deepseek-v4-pro
131
137
  ```
132
138
 
139
+ If your terminal uses a light background and parts of the UI look low contrast,
140
+ set the UI theme explicitly:
141
+
142
+ ```bash
143
+ deepy config theme light
144
+ ```
145
+
133
146
  ## Common Commands
134
147
 
135
148
  ```bash
136
149
  deepy --version
137
150
  deepy config setup
151
+ deepy config reset
152
+ deepy config theme
153
+ deepy config theme light
138
154
  deepy doctor
139
155
  deepy doctor --live --json
140
156
  deepy status
@@ -150,6 +166,8 @@ Inside the interactive terminal:
150
166
  /skills List available skills
151
167
  /new Start a fresh conversation
152
168
  /resume Pick a previous session
169
+ /theme Show or change UI theme
170
+ /reset Delete config and run setup again
153
171
  / Open the command menu
154
172
  Esc Interrupt the current model turn
155
173
  Ctrl+D Press twice to quit
@@ -180,6 +198,6 @@ assets live outside the package directory and are not included in the wheel.
180
198
 
181
199
  ## Release Status
182
200
 
183
- Deepy is preparing its first public `0.1.4` release. The current release path is
201
+ Deepy is preparing its first public `0.1.5` release. The current release path is
184
202
  GitHub + PyPI. Standalone binaries and npm wrappers can be added later, but the
185
203
  primary distribution is the Python CLI.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import asyncio
5
- import os
6
5
  import sys
7
6
  from pathlib import Path
8
7
  from typing import Sequence
@@ -10,8 +9,21 @@ from typing import Sequence
10
9
  import tomli_w
11
10
 
12
11
  from . import __version__
13
- from .config import Settings, load_settings, settings_to_toml_dict
14
- from .config.settings import DEFAULT_BASE_URL, DEFAULT_MODEL, DEFAULT_WEB_SEARCH_SEARXNG_URL
12
+ from .config import (
13
+ Settings,
14
+ load_settings,
15
+ settings_to_toml_dict,
16
+ ui_theme_from_selection,
17
+ ui_theme_number,
18
+ update_config_theme,
19
+ write_config,
20
+ )
21
+ from .config.settings import (
22
+ DEFAULT_BASE_URL,
23
+ DEFAULT_MODEL,
24
+ DEFAULT_UI_THEME,
25
+ UI_THEMES,
26
+ )
15
27
  from .errors import format_error_display
16
28
  from .llm.provider import build_provider_bundle
17
29
  from .llm.runner import DEFAULT_MAX_TURNS, run_prompt_once
@@ -20,6 +32,7 @@ from .skills import discover_skills, find_skill, format_skills_for_terminal, rea
20
32
  from .status import build_status_report, format_status_report, status_report_to_dict
21
33
  from .usage import TokenUsage, format_usage_line, usage_from_run_result
22
34
  from .ui import run_interactive
35
+ from .ui.styles import resolve_ui_palette
23
36
  from .utils import json as json_utils
24
37
 
25
38
 
@@ -42,9 +55,13 @@ def _build_parser() -> argparse.ArgumentParser:
42
55
  init_parser.add_argument("--api-key", help="DeepSeek API key.")
43
56
  init_parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name.")
44
57
  init_parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="OpenAI-compatible base URL.")
58
+ init_parser.add_argument("--theme", default=DEFAULT_UI_THEME, help="UI theme: auto, dark, or light.")
45
59
  init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
46
60
  setup_parser = config_sub.add_parser("setup", help="Interactively configure Deepy.")
47
61
  setup_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
62
+ config_sub.add_parser("reset", help="Delete local config and run interactive setup again.")
63
+ theme_parser = config_sub.add_parser("theme", help="Show or update terminal UI theme.")
64
+ theme_parser.add_argument("theme", nargs="?", help="Theme to save: auto, dark, or light.")
48
65
 
49
66
  doctor_parser = subparsers.add_parser("doctor", help="Validate local Deepy setup.")
50
67
  doctor_parser.add_argument("--json", action="store_true", help="Print JSON diagnostics.")
@@ -101,6 +118,7 @@ def _cmd_config_init(args: argparse.Namespace) -> int:
101
118
  api_key=args.api_key or "",
102
119
  model=args.model,
103
120
  base_url=args.base_url,
121
+ theme=args.theme,
104
122
  )
105
123
  print(f"Wrote {config_path}")
106
124
  return 0
@@ -110,7 +128,7 @@ def _cmd_config_setup(args: argparse.Namespace) -> int:
110
128
  config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
111
129
  if config_path.suffix == ".json":
112
130
  raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
113
- if config_path.exists() and not args.force:
131
+ if config_path.exists():
114
132
  existing = load_settings(config_path)
115
133
  else:
116
134
  existing = Settings(path=config_path)
@@ -119,11 +137,26 @@ def _cmd_config_setup(args: argparse.Namespace) -> int:
119
137
  api_key = _prompt_config_value("API key", default=existing.model.api_key or "", is_password=True)
120
138
  model = _prompt_config_value("Model", default=existing.model.name)
121
139
  base_url = _prompt_config_value("Base URL", default=existing.model.base_url)
122
- _write_config(config_path, api_key=api_key, model=model, base_url=base_url)
140
+ theme = _prompt_theme_value(default=existing.ui.theme)
141
+ _write_config(config_path, api_key=api_key, model=model, base_url=base_url, theme=theme)
123
142
  print(f"Wrote {config_path}")
124
143
  return 0
125
144
 
126
145
 
146
+ def _cmd_config_reset(args: argparse.Namespace) -> int:
147
+ config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
148
+ if config_path.suffix == ".json":
149
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
150
+ if config_path.exists():
151
+ config_path.unlink()
152
+ print(f"Removed {config_path}")
153
+ else:
154
+ print(f"No existing config at {config_path}")
155
+ print("Starting Deepy configuration setup...")
156
+ setup_args = argparse.Namespace(config=args.config, force=True)
157
+ return _cmd_config_setup(setup_args)
158
+
159
+
127
160
  def _prompt_config_value(label: str, *, default: str, is_password: bool = False) -> str:
128
161
  from prompt_toolkit import PromptSession
129
162
 
@@ -136,36 +169,33 @@ def _prompt_config_value(label: str, *, default: str, is_password: bool = False)
136
169
  return value or default
137
170
 
138
171
 
139
- def _write_config(config_path: Path, *, api_key: str, model: str, base_url: str) -> None:
140
- payload = {
141
- "model": {
142
- "name": model,
143
- "base_url": base_url,
144
- "api_key": api_key,
145
- "thinking": True,
146
- "reasoning_effort": "max",
147
- },
148
- "context": {
149
- "window_tokens": 1_048_576,
150
- "compact_trigger_ratio": 0.8,
151
- "compact_prompt_token_threshold": 838_861,
152
- },
153
- "logging": {
154
- "debug": False,
155
- },
156
- "notify": {
157
- "enabled": False,
158
- "command": "",
159
- },
160
- "tools": {
161
- "web_search": {
162
- "searxng_url": DEFAULT_WEB_SEARCH_SEARXNG_URL,
163
- },
164
- },
165
- }
166
- config_path.parent.mkdir(parents=True, exist_ok=True)
167
- config_path.write_text(tomli_w.dumps(payload), encoding="utf-8")
168
- os.chmod(config_path, 0o600)
172
+ def _prompt_theme_value(*, default: str = DEFAULT_UI_THEME) -> str:
173
+ print("UI theme:")
174
+ print("1. auto Detect when possible; falls back to dark")
175
+ print("2. dark Optimized for dark terminal backgrounds")
176
+ print("3. light Optimized for light terminal backgrounds")
177
+ value = _prompt_config_value("UI theme number", default=ui_theme_number(default))
178
+ return ui_theme_from_selection(value, default=default)
179
+
180
+
181
+ def _write_config(config_path: Path, *, api_key: str, model: str, base_url: str, theme: str) -> None:
182
+ write_config(config_path, api_key=api_key, model=model, base_url=base_url, theme=theme)
183
+
184
+
185
+ def _cmd_config_theme(args: argparse.Namespace) -> int:
186
+ settings = load_settings(args.config)
187
+ if args.theme is None:
188
+ palette = resolve_ui_palette(settings.ui.theme)
189
+ print(f"saved: {settings.ui.theme}")
190
+ print(f"resolved: {palette.name}")
191
+ return 0
192
+ if args.theme not in UI_THEMES:
193
+ print("Invalid theme. Usage: deepy config theme [auto|dark|light]", file=sys.stderr)
194
+ return 1
195
+ config_path = settings.path or (args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml")
196
+ update_config_theme(config_path, args.theme)
197
+ print(f"Saved UI theme: {args.theme}")
198
+ return 0
169
199
 
170
200
 
171
201
  def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
@@ -385,6 +415,10 @@ def main(argv: Sequence[str] | None = None) -> int:
385
415
  return _cmd_config_init(args)
386
416
  if args.config_command == "setup":
387
417
  return _cmd_config_setup(args)
418
+ if args.config_command == "reset":
419
+ return _cmd_config_reset(args)
420
+ if args.config_command == "theme":
421
+ return _cmd_config_theme(args)
388
422
  if args.command == "doctor":
389
423
  return _cmd_doctor(args)
390
424
  if args.command == "run":
@@ -404,6 +438,10 @@ def main(argv: Sequence[str] | None = None) -> int:
404
438
  setup_args = argparse.Namespace(config=args.config, force=True)
405
439
  _cmd_config_setup(setup_args)
406
440
  settings = load_settings(args.config)
441
+ if settings.path is not None and not settings.ui.theme_configured:
442
+ theme = _prompt_theme_value(default=settings.ui.theme)
443
+ update_config_theme(settings.path, theme)
444
+ settings = load_settings(args.config)
407
445
  return run_interactive(settings)
408
446
 
409
447
 
@@ -2,22 +2,40 @@ from __future__ import annotations
2
2
 
3
3
  from .settings import (
4
4
  ContextConfig,
5
+ DEFAULT_UI_THEME,
5
6
  DEFAULT_WEB_SEARCH_SEARXNG_URL,
6
7
  ModelConfig,
7
8
  Settings,
9
+ UI_THEME_OPTIONS,
10
+ UI_THEMES,
11
+ UiConfig,
8
12
  default_config_path,
13
+ is_valid_ui_theme,
9
14
  load_settings,
10
15
  mask_secret,
11
16
  settings_to_toml_dict,
17
+ update_config_theme,
18
+ ui_theme_from_selection,
19
+ ui_theme_number,
20
+ write_config,
12
21
  )
13
22
 
14
23
  __all__ = [
15
24
  "ContextConfig",
25
+ "DEFAULT_UI_THEME",
16
26
  "DEFAULT_WEB_SEARCH_SEARXNG_URL",
17
27
  "ModelConfig",
18
28
  "Settings",
29
+ "UI_THEME_OPTIONS",
30
+ "UI_THEMES",
31
+ "UiConfig",
19
32
  "default_config_path",
33
+ "is_valid_ui_theme",
20
34
  "load_settings",
21
35
  "mask_secret",
22
36
  "settings_to_toml_dict",
37
+ "update_config_theme",
38
+ "ui_theme_from_selection",
39
+ "ui_theme_number",
40
+ "write_config",
23
41
  ]
@@ -6,13 +6,18 @@ from dataclasses import asdict, dataclass, field
6
6
  from pathlib import Path
7
7
  from typing import Any, Mapping, Self
8
8
 
9
+ import tomli_w
10
+
9
11
  DEFAULT_MODEL = "deepseek-v4-pro"
10
12
  DEFAULT_BASE_URL = "https://api.deepseek.com"
11
13
  DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576
12
14
  DEFAULT_COMPACT_TRIGGER_RATIO = 0.8
13
15
  DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 838_861
14
16
  DEFAULT_WEB_SEARCH_SEARXNG_URL = "https://s.kirineko.tech/"
17
+ DEFAULT_UI_THEME = "auto"
15
18
  REASONING_EFFORTS = {"high", "max"}
19
+ UI_THEMES = {"auto", "dark", "light"}
20
+ UI_THEME_OPTIONS = (("1", "auto"), ("2", "dark"), ("3", "light"))
16
21
 
17
22
 
18
23
  def default_config_path() -> Path:
@@ -161,6 +166,19 @@ class ToolsConfig:
161
166
  return cls(web_search=WebSearchToolConfig.from_mapping(_as_mapping(raw.get("web_search"))))
162
167
 
163
168
 
169
+ @dataclass(frozen=True)
170
+ class UiConfig:
171
+ theme: str = DEFAULT_UI_THEME
172
+ theme_configured: bool = False
173
+
174
+ @classmethod
175
+ def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
176
+ theme = raw.get("theme")
177
+ if isinstance(theme, str) and theme.strip() in UI_THEMES:
178
+ return cls(theme=theme.strip(), theme_configured=True)
179
+ return cls()
180
+
181
+
164
182
  @dataclass(frozen=True)
165
183
  class Settings:
166
184
  model: ModelConfig = field(default_factory=ModelConfig)
@@ -168,6 +186,7 @@ class Settings:
168
186
  logging: LoggingConfig = field(default_factory=LoggingConfig)
169
187
  notify: NotifyConfig = field(default_factory=NotifyConfig)
170
188
  tools: ToolsConfig = field(default_factory=ToolsConfig)
189
+ ui: UiConfig = field(default_factory=UiConfig)
171
190
  path: Path | None = None
172
191
 
173
192
  @classmethod
@@ -184,6 +203,7 @@ class Settings:
184
203
  logging=LoggingConfig.from_mapping(_as_mapping(raw.get("logging"))),
185
204
  notify=NotifyConfig.from_mapping(_as_mapping(raw.get("notify"))),
186
205
  tools=ToolsConfig.from_mapping(_as_mapping(raw.get("tools"))),
206
+ ui=UiConfig.from_mapping(_as_mapping(raw.get("ui"))),
187
207
  path=path,
188
208
  )
189
209
 
@@ -208,6 +228,8 @@ def load_settings(
208
228
  def settings_to_toml_dict(settings: Settings, *, reveal_secret: bool = False) -> dict[str, Any]:
209
229
  data = _drop_empty(asdict(settings))
210
230
  data.pop("path", None)
231
+ if "ui" in data:
232
+ data["ui"].pop("theme_configured", None)
211
233
  api_key = settings.model.api_key
212
234
  if api_key:
213
235
  data["model"]["api_key"] = api_key if reveal_secret else mask_secret(api_key)
@@ -218,6 +240,99 @@ def settings_to_toml_dict(settings: Settings, *, reveal_secret: bool = False) ->
218
240
  return _drop_empty(data)
219
241
 
220
242
 
243
+ def is_valid_ui_theme(value: str) -> bool:
244
+ return value in UI_THEMES
245
+
246
+
247
+ def ui_theme_number(theme: str) -> str:
248
+ for number, value in UI_THEME_OPTIONS:
249
+ if value == theme:
250
+ return number
251
+ return "1"
252
+
253
+
254
+ def ui_theme_from_selection(value: str, *, default: str = DEFAULT_UI_THEME) -> str:
255
+ normalized = value.strip().lower()
256
+ if not normalized:
257
+ return default if is_valid_ui_theme(default) else DEFAULT_UI_THEME
258
+ if normalized in UI_THEMES:
259
+ return normalized
260
+ by_number = dict(UI_THEME_OPTIONS)
261
+ selected = by_number.get(normalized)
262
+ if selected is not None:
263
+ return selected
264
+ return default if is_valid_ui_theme(default) else DEFAULT_UI_THEME
265
+
266
+
267
+ def write_config(
268
+ config_path: Path,
269
+ *,
270
+ api_key: str,
271
+ model: str,
272
+ base_url: str,
273
+ theme: str,
274
+ ) -> None:
275
+ if not is_valid_ui_theme(theme):
276
+ raise ValueError("UI theme must be one of: auto, dark, light.")
277
+ path = config_path.expanduser()
278
+ if path.suffix == ".json":
279
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
280
+ payload = {
281
+ "model": {
282
+ "name": model,
283
+ "base_url": base_url,
284
+ "api_key": api_key,
285
+ "thinking": True,
286
+ "reasoning_effort": "max",
287
+ },
288
+ "context": {
289
+ "window_tokens": DEFAULT_CONTEXT_WINDOW_TOKENS,
290
+ "compact_trigger_ratio": DEFAULT_COMPACT_TRIGGER_RATIO,
291
+ "compact_prompt_token_threshold": DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD,
292
+ },
293
+ "logging": {
294
+ "debug": False,
295
+ },
296
+ "notify": {
297
+ "enabled": False,
298
+ "command": "",
299
+ },
300
+ "tools": {
301
+ "web_search": {
302
+ "searxng_url": DEFAULT_WEB_SEARCH_SEARXNG_URL,
303
+ },
304
+ },
305
+ "ui": {
306
+ "theme": theme,
307
+ },
308
+ }
309
+ path.parent.mkdir(parents=True, exist_ok=True)
310
+ path.write_text(tomli_w.dumps(payload), encoding="utf-8")
311
+ os.chmod(path, 0o600)
312
+
313
+
314
+ def update_config_theme(config_path: Path, theme: str) -> None:
315
+ if not is_valid_ui_theme(theme):
316
+ raise ValueError("UI theme must be one of: auto, dark, light.")
317
+ path = config_path.expanduser()
318
+ if path.suffix == ".json":
319
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
320
+ raw: dict[str, Any]
321
+ if path.exists():
322
+ with path.open("rb") as fh:
323
+ loaded = tomllib.load(fh)
324
+ raw = dict(loaded)
325
+ else:
326
+ raw = {}
327
+ ui = raw.get("ui")
328
+ ui_map = dict(ui) if isinstance(ui, Mapping) else {}
329
+ ui_map["theme"] = theme
330
+ raw["ui"] = ui_map
331
+ path.parent.mkdir(parents=True, exist_ok=True)
332
+ path.write_text(tomli_w.dumps(raw), encoding="utf-8")
333
+ os.chmod(path, 0o600)
334
+
335
+
221
336
  def _drop_empty(value: Any) -> Any:
222
337
  if isinstance(value, dict):
223
338
  result = {}