deepy-cli 0.2.25__tar.gz → 0.2.27__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 (118) hide show
  1. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/PKG-INFO +26 -13
  2. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/README.md +24 -11
  3. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/pyproject.toml +2 -2
  4. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/audit.py +2 -0
  6. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/cli.py +53 -7
  7. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/config/__init__.py +24 -0
  8. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/config/settings.py +134 -1
  9. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/test_shell.md +5 -3
  10. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/compaction.py +1 -1
  11. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/context.py +26 -1
  12. deepy_cli-0.2.27/src/deepy/llm/multimodal.py +279 -0
  13. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/provider.py +18 -1
  14. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/replay.py +11 -1
  15. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/runner.py +64 -5
  16. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/system.py +3 -4
  17. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/sessions/session.py +21 -0
  18. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/sessions/store_helpers.py +3 -10
  19. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/status.py +4 -0
  20. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/agents.py +38 -1
  21. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/builtin.py +269 -0
  22. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/test_shell.py +21 -6
  23. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/app.py +1239 -222
  24. deepy_cli-0.2.27/src/deepy/tui/commands.py +86 -0
  25. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/compat.py +1 -1
  26. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/diff.py +53 -9
  27. deepy_cli-0.2.27/src/deepy/tui/interaction_surfaces.py +121 -0
  28. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/runner.py +6 -4
  29. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/screens.py +201 -110
  30. deepy_cli-0.2.27/src/deepy/tui/theme.py +50 -0
  31. deepy_cli-0.2.27/src/deepy/tui/transcript.py +43 -0
  32. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/widgets.py +777 -140
  33. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/audit_approval_panel.py +19 -65
  34. deepy_cli-0.2.27/src/deepy/ui/image_input.py +527 -0
  35. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/local_command.py +1 -0
  36. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/markdown.py +15 -2
  37. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/message_view.py +99 -29
  38. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/prompt_input.py +102 -3
  39. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/slash_commands.py +78 -28
  40. deepy_cli-0.2.27/src/deepy/ui/syntax.py +88 -0
  41. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/terminal.py +220 -18
  42. deepy_cli-0.2.25/src/deepy/tui/commands.py +0 -105
  43. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/__main__.py +0 -0
  44. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/background_tasks.py +0 -0
  45. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/__init__.py +0 -0
  46. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  47. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  48. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  49. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/Read.md +0 -0
  50. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/Search.md +0 -0
  51. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/Update.md +0 -0
  52. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/WebFetch.md +0 -0
  53. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/WebSearch.md +0 -0
  54. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/Write.md +0 -0
  55. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/__init__.py +0 -0
  56. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/shell.md +0 -0
  57. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/task_list.md +0 -0
  58. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/task_output.md +0 -0
  59. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/task_stop.md +0 -0
  60. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/data/tools/todo_write.md +0 -0
  61. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/errors.py +0 -0
  62. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/input_suggestions.py +0 -0
  63. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/__init__.py +0 -0
  64. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/agent.py +0 -0
  65. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/cache_context.py +0 -0
  66. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/events.py +0 -0
  67. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/model_capabilities.py +0 -0
  68. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/llm/thinking.py +0 -0
  69. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/mcp.py +0 -0
  70. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/__init__.py +0 -0
  71. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/compact.py +0 -0
  72. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/init_agents.py +0 -0
  73. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/rules.py +0 -0
  74. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/runtime_context.py +0 -0
  75. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/prompts/tool_docs.py +0 -0
  76. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/session_cost.py +0 -0
  77. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/sessions/__init__.py +0 -0
  78. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/sessions/index.py +0 -0
  79. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/sessions/manager.py +0 -0
  80. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/skill_market.py +0 -0
  81. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/skills.py +0 -0
  82. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/subagents.py +0 -0
  83. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/todos.py +0 -0
  84. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/__init__.py +0 -0
  85. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/file_state.py +0 -0
  86. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/result.py +0 -0
  87. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/search.py +0 -0
  88. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/shell_output.py +0 -0
  89. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tools/shell_utils.py +0 -0
  90. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/__init__.py +0 -0
  91. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/tui/state.py +0 -0
  92. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/types/__init__.py +0 -0
  93. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/types/sdk.py +0 -0
  94. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/types/tool_payloads.py +0 -0
  95. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/__init__.py +0 -0
  96. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/app.py +0 -0
  97. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/ask_user_question.py +0 -0
  98. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/audit_approval_picker.py +0 -0
  99. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/exit_summary.py +0 -0
  100. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/file_mentions.py +0 -0
  101. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/loading_text.py +0 -0
  102. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/model_picker.py +0 -0
  103. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/prompt_buffer.py +0 -0
  104. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/session_list.py +0 -0
  105. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/session_picker.py +0 -0
  106. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/skill_picker.py +0 -0
  107. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/status_footer.py +0 -0
  108. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/styles.py +0 -0
  109. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/theme_picker.py +0 -0
  110. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/thinking_state.py +0 -0
  111. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/ui/welcome.py +0 -0
  112. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/update_check.py +0 -0
  113. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/usage.py +0 -0
  114. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/utils/__init__.py +0 -0
  115. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/utils/debug_logger.py +0 -0
  116. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/utils/error_logger.py +0 -0
  117. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/src/deepy/utils/json.py +0 -0
  118. {deepy_cli-0.2.25 → deepy_cli-0.2.27}/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.2.25
3
+ Version: 0.2.27
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
@@ -21,7 +21,7 @@ Requires-Dist: prompt-toolkit>=3.0,<4
21
21
  Requires-Dist: regex>=2026.5.9
22
22
  Requires-Dist: pyyaml>=6.0,<7
23
23
  Requires-Dist: rich>=14.2,<15
24
- Requires-Dist: textual>=8.2,<9
24
+ Requires-Dist: textual>=8.2.7,<9
25
25
  Requires-Dist: tiktoken>=0.9,<1
26
26
  Requires-Dist: tomli-w>=1
27
27
  Requires-Python: >=3.12
@@ -162,24 +162,35 @@ for direct local commands.
162
162
 
163
163
  ![Deepy web research workflow](asset/websearch.webp)
164
164
 
165
- ## Stable UI And Experimental TUI
165
+ ## Classic UI And Modern UI
166
166
 
167
- The default `deepy` command starts the stable Rich/prompt-toolkit terminal UI.
168
- The opt-in Textual interface is available with:
167
+ Deepy has two peer terminal interfaces. Classic UI is the Rich/prompt-toolkit
168
+ terminal UI, and Modern UI is the Textual terminal UI. The default `deepy`
169
+ command starts the UI saved in `ui.interface`; missing config defaults to
170
+ Classic UI with the dark theme. The compatibility command below still starts
171
+ Modern UI directly:
169
172
 
170
173
  ```bash
171
174
  deepy tui
172
175
  ```
173
176
 
174
- The TUI has a scrollable transcript, live thinking blocks, richer tool output
175
- blocks, slash-command and `@file` suggestions, status/help screens, and a
176
- Deepy-owned diff view. It remains experimental and may change between releases.
177
-
178
- ![Deepy Textual TUI](asset/deepy-tui.webp)
177
+ Modern UI has a compact scrollable transcript, lightweight status line,
178
+ Textual-native composer, live assistant/tool activity states, compact tool
179
+ summary rows, slash-command and `@file` suggestions, inline audit/model/theme
180
+ choices, status/help screens, and a Deepy-owned diff view. Use `/ui` in either
181
+ interface to change the default UI for the next `deepy` startup.
179
182
 
180
183
  `/status` shows session/project usage, context-window pressure, and DeepSeek
181
- balance in one panel. Exiting the TUI prints the same compact session summary
182
- as the stable terminal UI.
184
+ balance in one panel. Exiting Modern UI prints the same compact session summary
185
+ as Classic UI. Prompt text, image attachments, generated input
186
+ suggestions, slash suggestions, and file suggestions are separate composer
187
+ states, so UI-only attachment labels are not written into the editable prompt
188
+ buffer. Attached images appear in an attachment row with prompt-local keyboard
189
+ shortcuts: press Down to enter attachment selection, Left/Right to choose,
190
+ Backspace to remove, and Up to return to normal input. The shared `dark` /
191
+ `light` setting maps to curated
192
+ Textual themes in Modern UI, with `dark` defaulting to `tokyo-night`; Modern UI can
193
+ also save a Textual-only `ui.textual_theme` override from `/theme`.
183
194
 
184
195
  See [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) for the full feature
185
196
  comparison and current limitations.
@@ -263,7 +274,7 @@ MCP inheritance, and troubleshooting.
263
274
  | Tutorial videos | [docs/tutorial-videos.md](docs/tutorial-videos.md) | [docs/tutorial-videos.zh-CN.md](docs/tutorial-videos.zh-CN.md) |
264
275
  | MCP setup and troubleshooting | [docs/mcp.md](docs/mcp.md) | [docs/mcp.zh-CN.md](docs/mcp.zh-CN.md) |
265
276
  | Subagents and custom subagents | [docs/subagents.md](docs/subagents.md) | [docs/subagents.zh-CN.md](docs/subagents.zh-CN.md) |
266
- | Stable UI versus experimental TUI | [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) | [docs/deepy-ui-and-tui.zh-CN.md](docs/deepy-ui-and-tui.zh-CN.md) |
277
+ | Classic UI and Modern UI | [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) | [docs/deepy-ui-and-tui.zh-CN.md](docs/deepy-ui-and-tui.zh-CN.md) |
267
278
 
268
279
  ## Command Reference
269
280
 
@@ -301,6 +312,7 @@ Inside the interactive terminal:
301
312
  /<name> [request] Invoke a Skill directly
302
313
  /init Create or update project AGENTS.md
303
314
  /theme Show or change terminal UI theme
315
+ /ui Show or change Classic/Modern UI
304
316
  /ps Show managed background shell tasks
305
317
  /stop Choose background shell tasks to stop
306
318
  ```
@@ -331,6 +343,7 @@ compact_preserve_recent_messages = 2
331
343
  mode = "yolo" # normal, auto, or yolo
332
344
 
333
345
  [ui]
346
+ interface = "classic" # classic or modern
334
347
  theme = "dark" # dark or light
335
348
  ```
336
349
 
@@ -130,24 +130,35 @@ for direct local commands.
130
130
 
131
131
  ![Deepy web research workflow](asset/websearch.webp)
132
132
 
133
- ## Stable UI And Experimental TUI
133
+ ## Classic UI And Modern UI
134
134
 
135
- The default `deepy` command starts the stable Rich/prompt-toolkit terminal UI.
136
- The opt-in Textual interface is available with:
135
+ Deepy has two peer terminal interfaces. Classic UI is the Rich/prompt-toolkit
136
+ terminal UI, and Modern UI is the Textual terminal UI. The default `deepy`
137
+ command starts the UI saved in `ui.interface`; missing config defaults to
138
+ Classic UI with the dark theme. The compatibility command below still starts
139
+ Modern UI directly:
137
140
 
138
141
  ```bash
139
142
  deepy tui
140
143
  ```
141
144
 
142
- The TUI has a scrollable transcript, live thinking blocks, richer tool output
143
- blocks, slash-command and `@file` suggestions, status/help screens, and a
144
- Deepy-owned diff view. It remains experimental and may change between releases.
145
-
146
- ![Deepy Textual TUI](asset/deepy-tui.webp)
145
+ Modern UI has a compact scrollable transcript, lightweight status line,
146
+ Textual-native composer, live assistant/tool activity states, compact tool
147
+ summary rows, slash-command and `@file` suggestions, inline audit/model/theme
148
+ choices, status/help screens, and a Deepy-owned diff view. Use `/ui` in either
149
+ interface to change the default UI for the next `deepy` startup.
147
150
 
148
151
  `/status` shows session/project usage, context-window pressure, and DeepSeek
149
- balance in one panel. Exiting the TUI prints the same compact session summary
150
- as the stable terminal UI.
152
+ balance in one panel. Exiting Modern UI prints the same compact session summary
153
+ as Classic UI. Prompt text, image attachments, generated input
154
+ suggestions, slash suggestions, and file suggestions are separate composer
155
+ states, so UI-only attachment labels are not written into the editable prompt
156
+ buffer. Attached images appear in an attachment row with prompt-local keyboard
157
+ shortcuts: press Down to enter attachment selection, Left/Right to choose,
158
+ Backspace to remove, and Up to return to normal input. The shared `dark` /
159
+ `light` setting maps to curated
160
+ Textual themes in Modern UI, with `dark` defaulting to `tokyo-night`; Modern UI can
161
+ also save a Textual-only `ui.textual_theme` override from `/theme`.
151
162
 
152
163
  See [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) for the full feature
153
164
  comparison and current limitations.
@@ -231,7 +242,7 @@ MCP inheritance, and troubleshooting.
231
242
  | Tutorial videos | [docs/tutorial-videos.md](docs/tutorial-videos.md) | [docs/tutorial-videos.zh-CN.md](docs/tutorial-videos.zh-CN.md) |
232
243
  | MCP setup and troubleshooting | [docs/mcp.md](docs/mcp.md) | [docs/mcp.zh-CN.md](docs/mcp.zh-CN.md) |
233
244
  | Subagents and custom subagents | [docs/subagents.md](docs/subagents.md) | [docs/subagents.zh-CN.md](docs/subagents.zh-CN.md) |
234
- | Stable UI versus experimental TUI | [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) | [docs/deepy-ui-and-tui.zh-CN.md](docs/deepy-ui-and-tui.zh-CN.md) |
245
+ | Classic UI and Modern UI | [docs/deepy-ui-and-tui.md](docs/deepy-ui-and-tui.md) | [docs/deepy-ui-and-tui.zh-CN.md](docs/deepy-ui-and-tui.zh-CN.md) |
235
246
 
236
247
  ## Command Reference
237
248
 
@@ -269,6 +280,7 @@ Inside the interactive terminal:
269
280
  /<name> [request] Invoke a Skill directly
270
281
  /init Create or update project AGENTS.md
271
282
  /theme Show or change terminal UI theme
283
+ /ui Show or change Classic/Modern UI
272
284
  /ps Show managed background shell tasks
273
285
  /stop Choose background shell tasks to stop
274
286
  ```
@@ -299,6 +311,7 @@ compact_preserve_recent_messages = 2
299
311
  mode = "yolo" # normal, auto, or yolo
300
312
 
301
313
  [ui]
314
+ interface = "classic" # classic or modern
302
315
  theme = "dark" # dark or light
303
316
  ```
304
317
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.25"
3
+ version = "0.2.27"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -27,7 +27,7 @@ dependencies = [
27
27
  "regex>=2026.5.9",
28
28
  "pyyaml>=6.0,<7",
29
29
  "rich>=14.2,<15",
30
- "textual>=8.2,<9",
30
+ "textual>=8.2.7,<9",
31
31
  "tiktoken>=0.9,<1",
32
32
  "tomli-w>=1",
33
33
  ]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.25"
3
+ __version__ = "0.2.27"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -136,9 +136,11 @@ class PendingApproval:
136
136
  name: str
137
137
  tool_name: str
138
138
  arguments: str
139
+ call_id: str = ""
139
140
  agent_name: str = ""
140
141
  action_kind: str = "tool"
141
142
  server_name: str = ""
143
+ preflight: Mapping[str, Any] | None = None
142
144
 
143
145
 
144
146
  @dataclass(frozen=True)
@@ -22,14 +22,18 @@ from .config import (
22
22
  provider_info_for,
23
23
  settings_to_toml_dict,
24
24
  thinking_modes_for_provider,
25
+ ui_setup_from_selection,
26
+ ui_setup_number,
25
27
  ui_theme_from_selection,
26
28
  ui_theme_number,
27
29
  update_config_theme,
30
+ update_config_ui_choice,
28
31
  write_config,
29
32
  )
30
33
  from .config.settings import DEFAULT_UI_THEME, UI_THEMES
31
34
  from .errors import format_error_display
32
35
  from .llm.cache_context import format_cache_usage
36
+ from .llm.multimodal import redact_image_data_urls
33
37
  from .llm.provider import build_provider_bundle
34
38
  from .llm.runner import DEFAULT_MAX_TURNS, run_prompt_once
35
39
  from .sessions import DeepySession, list_session_entries
@@ -85,7 +89,7 @@ def _build_parser() -> argparse.ArgumentParser:
85
89
  run_parser.add_argument("--session", help="Resume an existing session id.")
86
90
  run_parser.add_argument("--skill", action="append", default=[], help="Load a skill by name.")
87
91
 
88
- subparsers.add_parser("tui", help="Start the experimental Textual TUI.")
92
+ subparsers.add_parser("tui", help="Start the Modern UI.")
89
93
 
90
94
  sessions_parser = subparsers.add_parser("sessions", help="Inspect project sessions.")
91
95
  sessions_sub = sessions_parser.add_subparsers(dest="sessions_command", required=True)
@@ -130,6 +134,7 @@ def _cmd_config_init(args: argparse.Namespace) -> int:
130
134
  base_url=args.base_url,
131
135
  thinking_mode=args.thinking,
132
136
  theme=args.theme,
137
+ interface="classic",
133
138
  )
134
139
  print(f"Wrote {config_path}")
135
140
  return 0
@@ -169,7 +174,10 @@ def _run_config_setup(config_path: Path) -> None:
169
174
  )
170
175
  base_url = _prompt_config_value("Base URL", default=base_default)
171
176
  thinking_mode = _prompt_thinking_mode_value(provider, default=existing.model.reasoning_mode)
172
- theme = _prompt_theme_value(default=existing.ui.theme)
177
+ interface, theme = _prompt_ui_choice_value(
178
+ default_interface=existing.ui.interface,
179
+ default_theme=existing.ui.theme,
180
+ )
173
181
  _write_config(
174
182
  config_path,
175
183
  api_key=api_key,
@@ -178,6 +186,7 @@ def _run_config_setup(config_path: Path) -> None:
178
186
  base_url=base_url,
179
187
  thinking_mode=thinking_mode,
180
188
  theme=theme,
189
+ interface=interface,
181
190
  )
182
191
 
183
192
 
@@ -388,6 +397,27 @@ def _prompt_theme_value(*, default: str = DEFAULT_UI_THEME) -> str:
388
397
  return ui_theme_from_selection(value, default=default)
389
398
 
390
399
 
400
+ def _prompt_ui_choice_value(
401
+ *,
402
+ default_interface: str = "classic",
403
+ default_theme: str = DEFAULT_UI_THEME,
404
+ ) -> tuple[str, str]:
405
+ print("UI:")
406
+ print("1. Classic UI + dark theme Default terminal UI")
407
+ print("2. Classic UI + light theme")
408
+ print("3. Modern UI + dark theme Textual UI")
409
+ print("4. Modern UI + light theme Textual UI")
410
+ value = _prompt_config_value(
411
+ "UI number",
412
+ default=ui_setup_number(default_interface, default_theme),
413
+ )
414
+ return ui_setup_from_selection(
415
+ value,
416
+ default_interface=default_interface,
417
+ default_theme=default_theme,
418
+ )
419
+
420
+
391
421
  def _write_config(
392
422
  config_path: Path,
393
423
  *,
@@ -396,6 +426,7 @@ def _write_config(
396
426
  model: str,
397
427
  base_url: str | None,
398
428
  theme: str,
429
+ interface: str,
399
430
  thinking_mode: str | None,
400
431
  ) -> None:
401
432
  write_config(
@@ -405,6 +436,7 @@ def _write_config(
405
436
  model=model,
406
437
  base_url=base_url,
407
438
  theme=theme,
439
+ interface=interface,
408
440
  thinking_mode=thinking_mode,
409
441
  )
410
442
 
@@ -614,7 +646,7 @@ def _cmd_sessions(args: argparse.Namespace) -> int:
614
646
  else 0,
615
647
  "cache_break_reason": entry.cache_break_reason if entry is not None else None,
616
648
  "cache_usage": entry.cache_usage if entry is not None else None,
617
- "items": items,
649
+ "items": redact_image_data_urls(items),
618
650
  }
619
651
  )
620
652
  )
@@ -669,21 +701,35 @@ def _ensure_interactive_settings(args: argparse.Namespace) -> Settings:
669
701
  raise SystemExit(1)
670
702
  settings = load_settings(args.config)
671
703
  if settings.path is not None and not settings.ui.theme_configured:
672
- theme = _prompt_theme_value(default=settings.ui.theme)
673
- update_config_theme(settings.path, theme)
704
+ interface, theme = _prompt_ui_choice_value(
705
+ default_interface=settings.ui.interface,
706
+ default_theme=settings.ui.theme,
707
+ )
708
+ update_config_ui_choice(settings.path, interface=interface, theme=theme)
674
709
  settings = load_settings(args.config)
675
710
  return settings
676
711
 
677
712
 
678
713
  def _cmd_tui(args: argparse.Namespace) -> int:
679
714
  if not sys.stdin.isatty():
680
- print("experimental TUI requires a TTY; use `deepy` for the stable terminal UI.", file=sys.stderr)
715
+ print("Modern UI requires a TTY; use `deepy run` for non-interactive prompts.", file=sys.stderr)
681
716
  return 1
682
717
  from deepy.tui import run_tui
683
718
 
684
719
  return run_tui(_ensure_interactive_settings(args), project_root=Path.cwd())
685
720
 
686
721
 
722
+ def _cmd_interactive(args: argparse.Namespace) -> int:
723
+ settings = _ensure_interactive_settings(args)
724
+ if settings.ui.interface == "modern":
725
+ if not sys.stdin.isatty():
726
+ raise RuntimeError("Modern UI requires a TTY.")
727
+ from deepy.tui import run_tui
728
+
729
+ return run_tui(settings, project_root=Path.cwd())
730
+ return run_interactive(settings)
731
+
732
+
687
733
  def main(argv: Sequence[str] | None = None) -> int:
688
734
  parser = _build_parser()
689
735
  args = parser.parse_args(argv)
@@ -714,7 +760,7 @@ def main(argv: Sequence[str] | None = None) -> int:
714
760
 
715
761
  if not sys.stdin.isatty():
716
762
  parser.error("interactive mode requires a TTY; use `deepy doctor` or `deepy config show`.")
717
- return run_interactive(_ensure_interactive_settings(args))
763
+ return _cmd_interactive(args)
718
764
 
719
765
 
720
766
  if __name__ == "__main__":
@@ -6,6 +6,7 @@ from .settings import (
6
6
  DEFAULT_BASE_URL,
7
7
  DEFAULT_COMPACT_PRESERVE_RECENT_MESSAGES,
8
8
  DEFAULT_INPUT_SUGGESTIONS_ENABLED,
9
+ DEFAULT_UI_INTERFACE,
9
10
  DEFAULT_OPENROUTER_BASE_URL,
10
11
  DEFAULT_PROVIDER,
11
12
  DEFAULT_RESERVED_CONTEXT_TOKENS,
@@ -27,6 +28,9 @@ from .settings import (
27
28
  TestShellToolConfig,
28
29
  UI_THEME_OPTIONS,
29
30
  UI_THEMES,
31
+ UI_INTERFACE_OPTIONS,
32
+ UI_INTERFACES,
33
+ UI_SETUP_OPTIONS,
30
34
  UI_VIEW_MODES,
31
35
  ToolsConfig,
32
36
  UiConfig,
@@ -45,6 +49,7 @@ from .settings import (
45
49
  is_valid_thinking_mode,
46
50
  is_valid_thinking_mode_for_provider,
47
51
  is_valid_ui_theme,
52
+ is_valid_ui_interface,
48
53
  is_valid_ui_view_mode,
49
54
  is_valid_reasoning_mode,
50
55
  is_valid_config_audit_mode,
@@ -57,8 +62,15 @@ from .settings import (
57
62
  update_config_model_settings,
58
63
  update_config_audit_mode,
59
64
  update_config_input_suggestions_enabled,
65
+ update_config_textual_theme,
60
66
  update_config_theme,
67
+ update_config_ui_choice,
68
+ update_config_ui_interface,
61
69
  update_config_view_mode,
70
+ ui_interface_from_selection,
71
+ ui_interface_number,
72
+ ui_setup_from_selection,
73
+ ui_setup_number,
62
74
  ui_theme_from_selection,
63
75
  ui_theme_number,
64
76
  write_config,
@@ -70,6 +82,7 @@ __all__ = [
70
82
  "DEFAULT_BASE_URL",
71
83
  "DEFAULT_COMPACT_PRESERVE_RECENT_MESSAGES",
72
84
  "DEFAULT_INPUT_SUGGESTIONS_ENABLED",
85
+ "DEFAULT_UI_INTERFACE",
73
86
  "DEFAULT_OPENROUTER_BASE_URL",
74
87
  "DEFAULT_PROVIDER",
75
88
  "DEFAULT_RESERVED_CONTEXT_TOKENS",
@@ -91,6 +104,9 @@ __all__ = [
91
104
  "TestShellToolConfig",
92
105
  "UI_THEME_OPTIONS",
93
106
  "UI_THEMES",
107
+ "UI_INTERFACE_OPTIONS",
108
+ "UI_INTERFACES",
109
+ "UI_SETUP_OPTIONS",
94
110
  "UI_VIEW_MODES",
95
111
  "ToolsConfig",
96
112
  "UiConfig",
@@ -109,6 +125,7 @@ __all__ = [
109
125
  "is_valid_thinking_mode",
110
126
  "is_valid_thinking_mode_for_provider",
111
127
  "is_valid_ui_theme",
128
+ "is_valid_ui_interface",
112
129
  "is_valid_ui_view_mode",
113
130
  "is_valid_reasoning_mode",
114
131
  "is_valid_config_audit_mode",
@@ -121,8 +138,15 @@ __all__ = [
121
138
  "update_config_model_settings",
122
139
  "update_config_audit_mode",
123
140
  "update_config_input_suggestions_enabled",
141
+ "update_config_textual_theme",
124
142
  "update_config_theme",
143
+ "update_config_ui_choice",
144
+ "update_config_ui_interface",
125
145
  "update_config_view_mode",
146
+ "ui_interface_from_selection",
147
+ "ui_interface_number",
148
+ "ui_setup_from_selection",
149
+ "ui_setup_number",
126
150
  "ui_theme_from_selection",
127
151
  "ui_theme_number",
128
152
  "write_config",
@@ -19,6 +19,7 @@ DEFAULT_RESERVED_CONTEXT_TOKENS = 50_000
19
19
  DEFAULT_COMPACT_PRESERVE_RECENT_MESSAGES = 2
20
20
  DEFAULT_WEB_SEARCH_SEARXNG_URL = "https://s.kirineko.tech/"
21
21
  DEFAULT_UI_THEME = "dark"
22
+ DEFAULT_UI_INTERFACE = "classic"
22
23
  DEFAULT_MCP_ENABLED = True
23
24
  DEFAULT_MCP_CONNECT_TIMEOUT_SECONDS = 10.0
24
25
  DEFAULT_MCP_CLEANUP_TIMEOUT_SECONDS = 10.0
@@ -50,6 +51,14 @@ THINKING_MODES = set(DEEPSEEK_REASONING_MODES) | set(SWITCH_ONLY_THINKING_MODES)
50
51
  PROVIDERS = {"deepseek", "openrouter", "xiaomi"}
51
52
  UI_THEMES = {"dark", "light"}
52
53
  UI_THEME_OPTIONS = (("1", "dark"), ("2", "light"))
54
+ UI_INTERFACES = {"classic", "modern"}
55
+ UI_INTERFACE_OPTIONS = (("1", "classic"), ("2", "modern"))
56
+ UI_SETUP_OPTIONS = (
57
+ ("1", "classic", "dark"),
58
+ ("2", "classic", "light"),
59
+ ("3", "modern", "dark"),
60
+ ("4", "modern", "light"),
61
+ )
53
62
  UI_VIEW_MODES = {"concise", "full"}
54
63
 
55
64
 
@@ -59,6 +68,7 @@ class ModelInfo:
59
68
  label: str
60
69
  description: str
61
70
  supports_thinking: bool = True
71
+ supports_image_input: bool = False
62
72
  default_reasoning_mode: str = "max"
63
73
 
64
74
 
@@ -102,6 +112,7 @@ OPENROUTER_MODEL_CATALOG = (
102
112
  name="xiaomi/mimo-v2.5",
103
113
  label="MiMo V2.5",
104
114
  description="Xiaomi MiMo V2.5 via OpenRouter.",
115
+ supports_image_input=True,
105
116
  default_reasoning_mode="enabled",
106
117
  ),
107
118
  )
@@ -116,6 +127,7 @@ XIAOMI_MODEL_CATALOG = (
116
127
  name="mimo-v2.5",
117
128
  label="MiMo V2.5",
118
129
  description="Xiaomi official MiMo V2.5.",
130
+ supports_image_input=True,
119
131
  default_reasoning_mode="enabled",
120
132
  ),
121
133
  )
@@ -327,6 +339,10 @@ def _as_str(value: Any, default: str = "") -> str:
327
339
  return value.strip() if isinstance(value, str) and value.strip() else default
328
340
 
329
341
 
342
+ def _as_optional_str(value: Any) -> str | None:
343
+ return value.strip() if isinstance(value, str) and value.strip() else None
344
+
345
+
330
346
  def _as_string_tuple(value: Any) -> tuple[str, ...]:
331
347
  if not isinstance(value, list):
332
348
  return ()
@@ -564,13 +580,19 @@ class McpConfig:
564
580
  @dataclass(frozen=True)
565
581
  class UiConfig:
566
582
  theme: str = DEFAULT_UI_THEME
583
+ interface: str = DEFAULT_UI_INTERFACE
567
584
  theme_configured: bool = False
585
+ textual_theme: str | None = None
568
586
  input_suggestions_enabled: bool = DEFAULT_INPUT_SUGGESTIONS_ENABLED
569
587
  view_mode: str = DEFAULT_UI_VIEW_MODE
570
588
 
571
589
  @classmethod
572
590
  def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
573
591
  theme = raw.get("theme")
592
+ interface = _as_str(raw.get("interface"), DEFAULT_UI_INTERFACE)
593
+ if interface not in UI_INTERFACES:
594
+ interface = DEFAULT_UI_INTERFACE
595
+ textual_theme = _as_optional_str(raw.get("textual_theme"))
574
596
  input_suggestions_enabled = _as_bool(
575
597
  raw.get("input_suggestions_enabled"),
576
598
  DEFAULT_INPUT_SUGGESTIONS_ENABLED,
@@ -581,18 +603,27 @@ class UiConfig:
581
603
  if isinstance(theme, str) and theme.strip() == "auto":
582
604
  return cls(
583
605
  theme=DEFAULT_UI_THEME,
606
+ interface=interface,
584
607
  theme_configured=True,
608
+ textual_theme=textual_theme,
585
609
  input_suggestions_enabled=input_suggestions_enabled,
586
610
  view_mode=view_mode,
587
611
  )
588
612
  if isinstance(theme, str) and theme.strip() in UI_THEMES:
589
613
  return cls(
590
614
  theme=theme.strip(),
615
+ interface=interface,
591
616
  theme_configured=True,
617
+ textual_theme=textual_theme,
592
618
  input_suggestions_enabled=input_suggestions_enabled,
593
619
  view_mode=view_mode,
594
620
  )
595
- return cls(input_suggestions_enabled=input_suggestions_enabled, view_mode=view_mode)
621
+ return cls(
622
+ textual_theme=textual_theme,
623
+ interface=interface,
624
+ input_suggestions_enabled=input_suggestions_enabled,
625
+ view_mode=view_mode,
626
+ )
596
627
 
597
628
 
598
629
  @dataclass(frozen=True)
@@ -669,6 +700,10 @@ def is_valid_ui_theme(value: str) -> bool:
669
700
  return value in UI_THEMES
670
701
 
671
702
 
703
+ def is_valid_ui_interface(value: str) -> bool:
704
+ return value in UI_INTERFACES
705
+
706
+
672
707
  def is_valid_ui_view_mode(value: str) -> bool:
673
708
  return value in UI_VIEW_MODES
674
709
 
@@ -713,6 +748,52 @@ def ui_theme_from_selection(value: str, *, default: str = DEFAULT_UI_THEME) -> s
713
748
  return default if is_valid_ui_theme(default) else DEFAULT_UI_THEME
714
749
 
715
750
 
751
+ def ui_interface_number(interface: str) -> str:
752
+ for number, value in UI_INTERFACE_OPTIONS:
753
+ if value == interface:
754
+ return number
755
+ return "1"
756
+
757
+
758
+ def ui_interface_from_selection(value: str, *, default: str = DEFAULT_UI_INTERFACE) -> str:
759
+ normalized = value.strip().lower()
760
+ if not normalized:
761
+ return default if is_valid_ui_interface(default) else DEFAULT_UI_INTERFACE
762
+ if normalized in UI_INTERFACES:
763
+ return normalized
764
+ by_number = dict(UI_INTERFACE_OPTIONS)
765
+ selected = by_number.get(normalized)
766
+ if selected is not None:
767
+ return selected
768
+ return default if is_valid_ui_interface(default) else DEFAULT_UI_INTERFACE
769
+
770
+
771
+ def ui_setup_number(interface: str, theme: str) -> str:
772
+ for number, option_interface, option_theme in UI_SETUP_OPTIONS:
773
+ if option_interface == interface and option_theme == theme:
774
+ return number
775
+ return "1"
776
+
777
+
778
+ def ui_setup_from_selection(
779
+ value: str,
780
+ *,
781
+ default_interface: str = DEFAULT_UI_INTERFACE,
782
+ default_theme: str = DEFAULT_UI_THEME,
783
+ ) -> tuple[str, str]:
784
+ normalized = value.strip().lower()
785
+ fallback = (
786
+ default_interface if is_valid_ui_interface(default_interface) else DEFAULT_UI_INTERFACE,
787
+ default_theme if is_valid_ui_theme(default_theme) else DEFAULT_UI_THEME,
788
+ )
789
+ if not normalized:
790
+ return fallback
791
+ for number, option_interface, option_theme in UI_SETUP_OPTIONS:
792
+ if normalized in {number, f"{option_interface}-{option_theme}", f"{option_interface} {option_theme}"}:
793
+ return option_interface, option_theme
794
+ return fallback
795
+
796
+
716
797
  def write_config(
717
798
  config_path: Path,
718
799
  *,
@@ -721,10 +802,13 @@ def write_config(
721
802
  model: str,
722
803
  base_url: str | None = None,
723
804
  theme: str,
805
+ interface: str = DEFAULT_UI_INTERFACE,
724
806
  thinking_mode: str | None = None,
725
807
  ) -> None:
726
808
  if not is_valid_ui_theme(theme):
727
809
  raise ValueError("UI theme must be one of: dark, light.")
810
+ if not is_valid_ui_interface(interface):
811
+ raise ValueError("UI interface must be one of: classic, modern.")
728
812
  if not is_supported_provider(provider):
729
813
  raise ValueError("Provider must be one of: deepseek, openrouter, xiaomi.")
730
814
  provider_info = provider_info_for(provider)
@@ -788,6 +872,7 @@ def write_config(
788
872
  },
789
873
  },
790
874
  "ui": {
875
+ "interface": interface,
791
876
  "theme": theme,
792
877
  "input_suggestions_enabled": DEFAULT_INPUT_SUGGESTIONS_ENABLED,
793
878
  "view_mode": DEFAULT_UI_VIEW_MODE,
@@ -863,6 +948,54 @@ def update_config_theme(config_path: Path, theme: str) -> None:
863
948
  ui = raw.get("ui")
864
949
  ui_map = dict(ui) if isinstance(ui, Mapping) else {}
865
950
  ui_map["theme"] = theme
951
+ ui_map.pop("textual_theme", None)
952
+ raw["ui"] = ui_map
953
+ _write_private_toml(path, raw)
954
+
955
+
956
+ def update_config_ui_interface(config_path: Path, interface: str) -> None:
957
+ if not is_valid_ui_interface(interface):
958
+ raise ValueError("UI interface must be one of: classic, modern.")
959
+ path = config_path.expanduser()
960
+ if path.suffix == ".json":
961
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
962
+ raw = _read_toml_mapping(path)
963
+ ui = raw.get("ui")
964
+ ui_map = dict(ui) if isinstance(ui, Mapping) else {}
965
+ ui_map["interface"] = interface
966
+ raw["ui"] = ui_map
967
+ _write_private_toml(path, raw)
968
+
969
+
970
+ def update_config_ui_choice(config_path: Path, *, interface: str, theme: str) -> None:
971
+ if not is_valid_ui_interface(interface):
972
+ raise ValueError("UI interface must be one of: classic, modern.")
973
+ if not is_valid_ui_theme(theme):
974
+ raise ValueError("UI theme must be one of: dark, light.")
975
+ path = config_path.expanduser()
976
+ if path.suffix == ".json":
977
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
978
+ raw = _read_toml_mapping(path)
979
+ ui = raw.get("ui")
980
+ ui_map = dict(ui) if isinstance(ui, Mapping) else {}
981
+ ui_map["interface"] = interface
982
+ ui_map["theme"] = theme
983
+ ui_map.pop("textual_theme", None)
984
+ raw["ui"] = ui_map
985
+ _write_private_toml(path, raw)
986
+
987
+
988
+ def update_config_textual_theme(config_path: Path, textual_theme: str) -> None:
989
+ theme = textual_theme.strip()
990
+ if not theme:
991
+ raise ValueError("Textual theme must not be empty.")
992
+ path = config_path.expanduser()
993
+ if path.suffix == ".json":
994
+ raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
995
+ raw = _read_toml_mapping(path)
996
+ ui = raw.get("ui")
997
+ ui_map = dict(ui) if isinstance(ui, Mapping) else {}
998
+ ui_map["textual_theme"] = theme
866
999
  raw["ui"] = ui_map
867
1000
  _write_private_toml(path, raw)
868
1001