deepy-cli 0.1.10__tar.gz → 0.1.11__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 (72) hide show
  1. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/PKG-INFO +2 -2
  2. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/README.md +1 -1
  3. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/pyproject.toml +1 -1
  4. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/__init__.py +1 -1
  5. deepy_cli-0.1.11/src/deepy/data/tools/AskUserQuestion.md +18 -0
  6. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/system.py +1 -0
  7. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/agents.py +5 -2
  8. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/ask_user_question.py +17 -1
  9. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/message_view.py +85 -8
  10. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/prompt_input.py +10 -4
  11. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/terminal.py +84 -23
  12. deepy_cli-0.1.10/src/deepy/data/tools/AskUserQuestion.md +0 -12
  13. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/__main__.py +0 -0
  14. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/cli.py +0 -0
  15. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/config/__init__.py +0 -0
  16. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/config/settings.py +0 -0
  17. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/__init__.py +0 -0
  18. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/WebFetch.md +0 -0
  19. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/WebSearch.md +0 -0
  20. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/__init__.py +0 -0
  21. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/edit.md +0 -0
  22. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/modify.md +0 -0
  23. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/read.md +0 -0
  24. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/shell.md +0 -0
  25. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/write.md +0 -0
  26. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/errors.py +0 -0
  27. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/__init__.py +0 -0
  28. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/agent.py +0 -0
  29. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/compaction.py +0 -0
  30. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/context.py +0 -0
  31. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/events.py +0 -0
  32. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/model_capabilities.py +0 -0
  33. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/provider.py +0 -0
  34. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/replay.py +0 -0
  35. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/runner.py +0 -0
  36. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/thinking.py +0 -0
  37. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/__init__.py +0 -0
  38. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/compact.py +0 -0
  39. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/rules.py +0 -0
  40. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/runtime_context.py +0 -0
  41. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/tool_docs.py +0 -0
  42. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/__init__.py +0 -0
  43. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/jsonl.py +0 -0
  44. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/manager.py +0 -0
  45. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/skills.py +0 -0
  46. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/status.py +0 -0
  47. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/__init__.py +0 -0
  48. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/builtin.py +0 -0
  49. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/file_state.py +0 -0
  50. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/result.py +0 -0
  51. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/shell_utils.py +0 -0
  52. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/__init__.py +0 -0
  53. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/app.py +0 -0
  54. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/exit_summary.py +0 -0
  55. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/loading_text.py +0 -0
  56. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/markdown.py +0 -0
  57. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/model_picker.py +0 -0
  58. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/prompt_buffer.py +0 -0
  59. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/session_list.py +0 -0
  60. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/session_picker.py +0 -0
  61. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/slash_commands.py +0 -0
  62. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/styles.py +0 -0
  63. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/theme_picker.py +0 -0
  64. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/thinking_state.py +0 -0
  65. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/welcome.py +0 -0
  66. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/update_check.py +0 -0
  67. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/usage.py +0 -0
  68. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/__init__.py +0 -0
  69. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/debug_logger.py +0 -0
  70. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/error_logger.py +0 -0
  71. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/json.py +0 -0
  72. {deepy_cli-0.1.10 → deepy_cli-0.1.11}/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.10
3
+ Version: 0.1.11
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
@@ -238,5 +238,5 @@ assets live outside the package directory and are not included in the wheel.
238
238
 
239
239
  ## Release Status
240
240
 
241
- Deepy `0.1.10` is released through GitHub and PyPI. Standalone binaries and npm
241
+ Deepy `0.1.11` is released through GitHub and PyPI. Standalone binaries and npm
242
242
  wrappers can be added later, but the primary distribution is the Python CLI.
@@ -210,5 +210,5 @@ assets live outside the package directory and are not included in the wheel.
210
210
 
211
211
  ## Release Status
212
212
 
213
- Deepy `0.1.10` is released through GitHub and PyPI. Standalone binaries and npm
213
+ Deepy `0.1.11` is released through GitHub and PyPI. Standalone binaries and npm
214
214
  wrappers can be added later, but the primary distribution is the Python CLI.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.1.10"
3
+ version = "0.1.11"
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.10"
3
+ __version__ = "0.1.11"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -0,0 +1,18 @@
1
+ ## AskUserQuestion
2
+
3
+ 当澄清信息会明显影响结果时,使用此工具暂停执行并询问用户:意图不明确、
4
+ 范围不清楚、用户偏好会影响实现、存在多个实现路线或高影响取舍、下一步需要
5
+ 用户批准或决策。
6
+
7
+ 如果用户使用中文提问,问题、选项和说明也优先使用中文;否则匹配用户的语言。
8
+ 若用户使用中文,visible thinking/reasoning 也必须使用中文,除非用户明确要求其他语言。
9
+ 通常一次只问一个关键问题。若你推荐某个选项,把它放在第一位并在 label 末尾
10
+ 标注 `(Recommended)` 或中文等价表达。不要为了低影响细节提问;可以合理假设时
11
+ 继续推进并简短说明假设。
12
+
13
+ Args: `questions` (non-empty array). Each question needs `question` and non-empty `options`;
14
+ each option needs `label` and may include `description`. Use `multiSelect=true` only when
15
+ multiple choices are allowed.
16
+
17
+ Returns standard JSON with `awaitUserResponse=true`, `metadata.kind="ask_user_question"`,
18
+ and normalized questions.
@@ -47,6 +47,7 @@ Core rules:
47
47
  - Use `modify` for file changes: `content` only creates new files; existing files use `old_string`/`new_string`.
48
48
  - After project generators create scaffold files, read and edit the generated block instead of replacing the file.
49
49
  - Run shell commands using the Runtime context's command dialect and path style: `powershell` -> PowerShell with Windows paths; `cmd` -> cmd; `posix` -> POSIX shell.
50
+ - Match visible thinking/reasoning language to the user's latest natural language. If the user asks in Chinese, you MUST write visible thinking/reasoning in Chinese unless they explicitly request another language. Do not switch visible thinking/reasoning to English for Chinese requests.
50
51
  - Ask when clarification would materially improve the result: ambiguous intent, unclear scope,
51
52
  user preferences, high-impact trade-offs, or required approval. For low-impact details,
52
53
  proceed with a reasonable assumption and state it briefly.
@@ -61,8 +61,11 @@ def build_function_tools(runtime: ToolRuntime) -> list[object]:
61
61
  FunctionTool(
62
62
  name="AskUserQuestion",
63
63
  description=(
64
- "When the task has ambiguities or multiple implementation approaches, use this tool "
65
- "to pause execution and ask the user a question to get clarification or make a decision."
64
+ "当用户意图、范围、偏好、实现路线、高影响取舍或必要批准会明显影响结果时,"
65
+ "use this tool to pause and ask a concise question. Match the user's language; "
66
+ "for Chinese requests, ask in Chinese. If one option is recommended, list it first "
67
+ "and mark it as recommended. Do not ask for low-impact details when a reasonable "
68
+ "assumption can keep progress moving."
66
69
  ),
67
70
  params_json_schema=ASK_USER_QUESTION_SCHEMA,
68
71
  on_invoke_tool=invoke_ask_user_question,
@@ -40,6 +40,7 @@ class AskUserQuestionOptionEntry:
40
40
  def build_options(question: AskUserQuestionItem | None) -> list[AskUserQuestionOptionEntry]:
41
41
  if question is None:
42
42
  return []
43
+ custom_label, custom_description = _custom_answer_text(question.question)
43
44
  return [
44
45
  *[
45
46
  AskUserQuestionOptionEntry(
@@ -49,7 +50,12 @@ def build_options(question: AskUserQuestionItem | None) -> list[AskUserQuestionO
49
50
  )
50
51
  for option in question.options
51
52
  ],
52
- AskUserQuestionOptionEntry(label="Other / custom answer", value=OTHER_VALUE, is_other=True),
53
+ AskUserQuestionOptionEntry(
54
+ label=custom_label,
55
+ value=OTHER_VALUE,
56
+ description=custom_description,
57
+ is_other=True,
58
+ ),
53
59
  ]
54
60
 
55
61
 
@@ -177,6 +183,16 @@ def _stripped_string(value: Any) -> str:
177
183
  return value.strip() if isinstance(value, str) else ""
178
184
 
179
185
 
186
+ def _custom_answer_text(question: str) -> tuple[str, str]:
187
+ if _contains_cjk(question):
188
+ return "自定义回答", "输入自己的答案。"
189
+ return "Custom answer", "Type your own answer."
190
+
191
+
192
+ def _contains_cjk(value: str) -> bool:
193
+ return any("\u4e00" <= char <= "\u9fff" for char in value)
194
+
195
+
180
196
  def _escape_answer_part(value: str) -> str:
181
197
  normalized = " ".join(value.split())
182
198
  return normalized.replace("\\", "\\\\").replace('"', '\\"')
@@ -25,6 +25,16 @@ MAX_DIFF_LINES = 80
25
25
  MAX_SYNTAX_SAMPLE_CHARS = 4_000
26
26
  MAX_SYNTAX_SAMPLE_LINES = 80
27
27
  DIFF_PREVIEW_TOOLS = {"edit", "write"}
28
+ TOOL_DISPLAY_LABELS = {
29
+ "AskUserQuestion": "AskUserQuestion",
30
+ "WebFetch": "WebFetch",
31
+ "WebSearch": "WebSearch",
32
+ "edit": "Modify",
33
+ "modify": "Modify",
34
+ "write": "Write",
35
+ "read": "Read",
36
+ "shell": "Shell",
37
+ }
28
38
  ROLE_TITLES = {
29
39
  "user": "You",
30
40
  "assistant": "Deepy",
@@ -88,7 +98,9 @@ def parse_tool_output(output: str) -> ToolOutputView:
88
98
  await_user_response = bool(payload.get("awaitUserResponse"))
89
99
 
90
100
  detail = (error or path or _first_nonempty_line(text_output) or "").strip()
91
- summary = f"{name} {status}" + (f" - {_truncate(detail)}" if detail else "")
101
+ summary = f"{format_tool_display_label(name)} {status}" + (
102
+ f" - {_truncate(detail)}" if detail else ""
103
+ )
92
104
  return ToolOutputView(
93
105
  name=name,
94
106
  ok=ok_value,
@@ -119,7 +131,7 @@ def format_tool_call_summary(
119
131
  {"name": tool_name, "arguments": arguments or ""},
120
132
  project_root=project_root,
121
133
  )
122
- return f"{tool_name} {snippet}".strip()
134
+ return f"{format_tool_display_label(tool_name)} {snippet}".strip()
123
135
 
124
136
 
125
137
  def format_tool_progress_summary(
@@ -127,11 +139,24 @@ def format_tool_progress_summary(
127
139
  output: str,
128
140
  ) -> str:
129
141
  view = parse_tool_output(output)
130
- base = call_summary.strip() or view.name
142
+ base = call_summary.strip() or format_tool_display_label(view.name)
131
143
  detail = _tool_progress_detail(view)
132
144
  return f"{base} {view.status}" + (f" - {detail}" if detail else "")
133
145
 
134
146
 
147
+ def format_tool_display_name(name: str) -> str:
148
+ if name in TOOL_DISPLAY_LABELS:
149
+ return TOOL_DISPLAY_LABELS[name]
150
+ stripped = name.strip()
151
+ if not stripped:
152
+ return "Tool"
153
+ return _display_title(stripped)
154
+
155
+
156
+ def format_tool_display_label(name: str) -> str:
157
+ return f"[{format_tool_display_name(name)}]"
158
+
159
+
135
160
  def tool_diff_preview(output: str, *, max_lines: int = MAX_DIFF_LINES) -> str | None:
136
161
  view = parse_tool_output(output)
137
162
  diff = _tool_diff_text(view)
@@ -165,9 +190,8 @@ def render_tool_diff_preview(
165
190
  if not preview.lines:
166
191
  return None
167
192
  syntax = _diff_preview_syntax(preview, palette)
168
- label = "Wrote" if view.name.lower() == "write" else "Edited"
169
193
  return Group(
170
- render_diff_preview_header(preview, label=label, palette=palette),
194
+ render_diff_preview_header(preview, tool_name=view.name, palette=palette),
171
195
  *(render_diff_preview_line(line, palette=palette, width=width, syntax=syntax) for line in preview.lines),
172
196
  )
173
197
 
@@ -185,14 +209,15 @@ def parse_diff_preview_view(diff_preview: str, *, path: str | None = None) -> Di
185
209
  def render_diff_preview_header(
186
210
  preview: DiffPreview,
187
211
  *,
188
- label: str,
212
+ tool_name: str,
189
213
  palette: UiPalette | None = None,
190
214
  ) -> Text:
191
215
  palette = palette or DARK_PALETTE
216
+ label = format_tool_display_label(tool_name)
192
217
  if preview.path:
193
218
  label = f"{label} {preview.path}"
194
219
  label = f"{label} (+{preview.added} -{preview.removed})"
195
- return Text(f"• {label}", style=f"bold {palette.info}")
220
+ return _tool_label_line(label, style=palette.info, bullet=True)
196
221
 
197
222
 
198
223
  def render_diff_preview_line(
@@ -431,13 +456,33 @@ def render_tool_output(
431
456
  ) -> Group:
432
457
  palette = palette or DARK_PALETTE
433
458
  view = parse_tool_output(output)
434
- parts: list[Any] = [Text(view.summary, style=status_style(view.ok, palette))]
459
+ parts: list[Any] = [_render_tool_summary(view, palette)]
460
+ shell_output = render_shell_output_block(output, palette=palette)
461
+ if shell_output:
462
+ parts.append(shell_output)
435
463
  diff = render_tool_diff_preview(output, palette=palette, width=width)
436
464
  if diff:
437
465
  parts.append(diff)
438
466
  return Group(*parts)
439
467
 
440
468
 
469
+ def render_shell_output_block(
470
+ output: str,
471
+ *,
472
+ palette: UiPalette | None = None,
473
+ ) -> Panel | None:
474
+ palette = palette or DARK_PALETTE
475
+ view = parse_tool_output(output)
476
+ if view.name != "shell" or not view.output:
477
+ return None
478
+ return Panel(
479
+ Text(view.output.rstrip("\n"), style=palette.markdown_code_block),
480
+ title=format_tool_display_label("shell"),
481
+ border_style=palette.tool,
482
+ expand=False,
483
+ )
484
+
485
+
441
486
  def render_message(
442
487
  message: dict[str, Any],
443
488
  *,
@@ -471,6 +516,31 @@ def _tool_diff_text(view: ToolOutputView) -> str | None:
471
516
  return view.diff_preview or view.diff
472
517
 
473
518
 
519
+ def _render_tool_summary(view: ToolOutputView, palette: UiPalette) -> Text:
520
+ style = status_style(view.ok, palette)
521
+ label = format_tool_display_label(view.name)
522
+ if not view.summary.startswith(label):
523
+ return Text(view.summary, style=style)
524
+ return _tool_label_line(view.summary, style=style)
525
+
526
+
527
+ def _tool_label_line(text: str, *, style: str, bullet: bool = False) -> Text:
528
+ label_match = re.match(r"(\[[^\]]+\])(\s?.*)", text, flags=re.DOTALL)
529
+ if not label_match:
530
+ return Text(text, style=style)
531
+ label, detail = label_match.groups()
532
+ parts = []
533
+ if bullet:
534
+ parts.append(("• ", style))
535
+ parts.extend(
536
+ [
537
+ (label, f"bold underline {style}"),
538
+ (detail, style),
539
+ ]
540
+ )
541
+ return Text.assemble(*parts)
542
+
543
+
474
544
  def _parse_hunk_header(line: str) -> tuple[int, int] | None:
475
545
  match = re.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
476
546
  if not match:
@@ -621,6 +691,13 @@ def _string_or_none(value: Any) -> str | None:
621
691
  return None
622
692
 
623
693
 
694
+ def _display_title(value: str) -> str:
695
+ parts = [part for part in re.split(r"[_\-\s]+", value) if part]
696
+ if not parts:
697
+ return "Tool"
698
+ return "".join(part[:1].upper() + part[1:] for part in parts)
699
+
700
+
624
701
  def _first_nonempty_line(value: str) -> str | None:
625
702
  for line in value.splitlines():
626
703
  stripped = line.strip()
@@ -24,8 +24,8 @@ DEFAULT_PROMPT_HISTORY = Path.home() / ".deepy" / "prompt-history.txt"
24
24
  CTRL_D_EXIT_CONFIRM_SIGNAL = "\0deepy:ctrl-d-exit-confirm\0"
25
25
  PROMPT_TOOLBAR_BACKGROUND = "#161821"
26
26
  PROMPT_TOOLBAR_FOREGROUND = "#a6adc8"
27
- PROMPT_TOOLBAR_HELP = "Enter send · Shift+Enter newline · / commands · Esc interrupt · Ctrl+D twice exit"
28
- WINDOWS_PROMPT_TOOLBAR_HELP = "Enter send · Ctrl+J newline · / commands · Esc interrupt · Ctrl+D twice exit"
27
+ PROMPT_TOOLBAR_HELP = "Shift+Enter newline · Ctrl+D twice exit"
28
+ WINDOWS_PROMPT_TOOLBAR_HELP = "Ctrl+J newline · Ctrl+D twice exit"
29
29
  PROMPT_MESSAGE: AnyFormattedText = [("class:prompt", "> ")]
30
30
  PROMPT_PLACEHOLDER: AnyFormattedText = [("class:placeholder", "Type your message...")]
31
31
  PROMPT_TOOLBAR: AnyFormattedText = [("class:toolbar.help", PROMPT_TOOLBAR_HELP)]
@@ -139,11 +139,17 @@ def prompt_for_input(
139
139
  ).strip()
140
140
 
141
141
 
142
- def build_prompt_toolbar(context_status: str = "") -> AnyFormattedText:
142
+ def build_prompt_toolbar(
143
+ context_status: str = "",
144
+ *,
145
+ platform_name: str | None = None,
146
+ ) -> AnyFormattedText:
143
147
  if not context_status:
144
- return prompt_toolbar()
148
+ return prompt_toolbar(platform_name)
145
149
  return [
146
150
  ("class:toolbar.context", context_status),
151
+ ("class:toolbar.separator", " · "),
152
+ *prompt_toolbar(platform_name),
147
153
  ]
148
154
 
149
155
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import contextlib
5
5
  import os
6
+ import re
6
7
  import select
7
8
  import threading
8
9
  import time
@@ -61,9 +62,11 @@ from deepy.ui.ask_user_question import normalize_questions
61
62
  from deepy.ui.exit_summary import build_exit_summary_text
62
63
  from deepy.ui.message_view import (
63
64
  build_thinking_summary,
65
+ format_tool_display_label,
64
66
  format_tool_call_summary,
65
67
  format_tool_progress_summary,
66
68
  parse_tool_output,
69
+ render_shell_output_block,
67
70
  render_tool_diff_preview,
68
71
  )
69
72
  from deepy.ui.markdown import render_markdown
@@ -442,8 +445,8 @@ class TerminalStreamRenderer:
442
445
  )
443
446
  self.status_detail = ""
444
447
  self.pending_tool_calls: dict[str, ToolCallDisplay] = {}
445
- self.reasoning_text = ""
446
- self.reasoning_flushed = False
448
+ self.reasoning_started = False
449
+ self.reasoning_buffer = ""
447
450
 
448
451
  def __call__(self, event: DeepyStreamEvent) -> None:
449
452
  _print_stream_event(
@@ -456,11 +459,19 @@ class TerminalStreamRenderer:
456
459
  )
457
460
 
458
461
  def add_reasoning(self, text: str) -> None:
459
- if self.reasoning_flushed:
460
- self.reasoning_text = ""
461
- self.reasoning_flushed = False
462
- self.reasoning_text += text
463
- summary = build_thinking_summary(self.reasoning_text)
462
+ if not text:
463
+ return
464
+ if not self.reasoning_started:
465
+ self.console.print(
466
+ Text.assemble(
467
+ ("• ", self.palette.muted),
468
+ (format_tool_display_label("Thinking"), f"bold {self.palette.muted}"),
469
+ ),
470
+ )
471
+ self.reasoning_started = True
472
+ self.reasoning_buffer += text
473
+ self._print_stable_reasoning()
474
+ summary = build_thinking_summary(self.reasoning_buffer or text)
464
475
  if self.status is not None and summary:
465
476
  self.update_status(f"Thinking {summary}")
466
477
 
@@ -481,19 +492,17 @@ class TerminalStreamRenderer:
481
492
  )
482
493
 
483
494
  def flush(self) -> None:
484
- if self.reasoning_flushed:
485
- return
486
- summary = build_thinking_summary(self.reasoning_text)
487
- if not summary:
488
- return
489
- self.console.print(
490
- Text.assemble(
491
- ("• ", self.palette.muted),
492
- ("Thinking ", f"bold {self.palette.muted}"),
493
- (summary, self.palette.muted),
494
- )
495
+ self._print_stable_reasoning(force=True)
496
+ self.reasoning_started = False
497
+ self.reasoning_buffer = ""
498
+
499
+ def _print_stable_reasoning(self, *, force: bool = False) -> None:
500
+ text, self.reasoning_buffer = _split_stable_reasoning_text(
501
+ self.reasoning_buffer,
502
+ force=force,
495
503
  )
496
- self.reasoning_flushed = True
504
+ if text:
505
+ self.console.print(Text(text.rstrip("\n"), style=self.palette.muted))
497
506
 
498
507
 
499
508
  def _handle_slash_command(
@@ -1516,9 +1525,12 @@ def _print_stream_event(
1516
1525
  view = parse_tool_output(event.text)
1517
1526
  call_id = _string_payload(event.payload.get("call_id"))
1518
1527
  call = pending_tool_calls.pop(call_id, None) if pending_tool_calls is not None else None
1519
- call_summary = call.summary if call is not None else view.name
1528
+ call_summary = call.summary if call is not None else ""
1520
1529
  summary = format_tool_progress_summary(call_summary, event.text)
1521
1530
  console.print(_status_line(summary, status_style(view.ok, palette)))
1531
+ shell_output = render_shell_output_block(event.text, palette=palette)
1532
+ if shell_output:
1533
+ console.print(shell_output)
1522
1534
  diff = render_tool_diff_preview(event.text, palette=palette, width=console.width)
1523
1535
  if diff:
1524
1536
  console.print(diff)
@@ -1536,8 +1548,30 @@ def _string_payload(value: object) -> str:
1536
1548
  return value if isinstance(value, str) else ""
1537
1549
 
1538
1550
 
1551
+ _REASONING_BUFFER_TARGET_CHARS = 180
1552
+
1553
+
1554
+ def _split_stable_reasoning_text(text: str, *, force: bool = False) -> tuple[str, str]:
1555
+ if force:
1556
+ return text, ""
1557
+ newline_index = text.rfind("\n")
1558
+ if newline_index >= 0:
1559
+ return text[: newline_index + 1], text[newline_index + 1 :]
1560
+ if len(text) >= _REASONING_BUFFER_TARGET_CHARS:
1561
+ return text, ""
1562
+ return "", text
1563
+
1564
+
1539
1565
  def _status_line(text: str, style: str) -> Text:
1540
- return Text.assemble(("• ", style), (text, f"bold {style}"))
1566
+ label_match = re.match(r"(\[[^\]]+\])(\s?.*)", text, flags=re.DOTALL)
1567
+ if label_match:
1568
+ label, detail = label_match.groups()
1569
+ return Text.assemble(
1570
+ ("• ", style),
1571
+ (label, f"bold underline {style}"),
1572
+ (detail, style),
1573
+ )
1574
+ return Text.assemble(("• ", style), (text, style))
1541
1575
 
1542
1576
 
1543
1577
  def _collect_pending_question_response(
@@ -1569,13 +1603,20 @@ def _prompt_for_question(
1569
1603
  detail = f" - {option.description}" if option.description else ""
1570
1604
  console.print(f"{index}. {option.label}{detail}")
1571
1605
  prompt = (
1572
- "Answer numbers separated by commas, text, or empty to decline"
1606
+ "Answer numbers separated by commas, custom text, or empty to decline"
1573
1607
  if question.multi_select
1574
- else "Answer number, text, or empty to decline"
1608
+ else "Answer number, custom text, or empty to decline"
1575
1609
  )
1576
1610
  raw_answer = input_func(prompt).strip()
1577
1611
  if not raw_answer:
1578
1612
  return None
1613
+ direct_option = None if question.multi_select else _option_from_token(options, raw_answer)
1614
+ if direct_option is not None and direct_option.is_other:
1615
+ custom_answer = input_func(_custom_answer_prompt(direct_option)).strip()
1616
+ return build_answer_for_question(question, direct_option, [], custom_answer)
1617
+ if question.multi_select and _multi_select_needs_custom_text(options, raw_answer):
1618
+ custom_answer = input_func(_custom_answer_prompt(options[-1])).strip()
1619
+ raw_answer = f"{raw_answer}, {custom_answer}" if custom_answer else raw_answer
1579
1620
  return _answer_question_from_text(question, raw_answer)
1580
1621
 
1581
1622
 
@@ -1606,6 +1647,26 @@ def _answer_question_from_text(question: AskUserQuestionItem, raw_answer: str) -
1606
1647
  return build_answer_for_question(question, option, [], other_text)
1607
1648
 
1608
1649
 
1650
+ def _multi_select_needs_custom_text(
1651
+ options: list[AskUserQuestionOptionEntry],
1652
+ raw_answer: str,
1653
+ ) -> bool:
1654
+ tokens = [part.strip() for part in raw_answer.split(",") if part.strip()]
1655
+ saw_other = False
1656
+ saw_custom_text = False
1657
+ for token in tokens:
1658
+ option = _option_from_token(options, token)
1659
+ if option is not None and option.is_other:
1660
+ saw_other = True
1661
+ elif option is None:
1662
+ saw_custom_text = True
1663
+ return saw_other and not saw_custom_text
1664
+
1665
+
1666
+ def _custom_answer_prompt(option: AskUserQuestionOptionEntry) -> str:
1667
+ return "自定义回答" if option.label.startswith("自定义") else "Custom answer"
1668
+
1669
+
1609
1670
  def _option_from_token(
1610
1671
  options: list[AskUserQuestionOptionEntry],
1611
1672
  token: str,
@@ -1,12 +0,0 @@
1
- ## AskUserQuestion
2
-
3
- Ask the user when clarification would materially improve the result: ambiguous intent,
4
- unclear scope, user preferences, high-impact trade-offs, or required approval. Do not
5
- ask for low-impact details when a reasonable assumption can keep progress moving.
6
-
7
- Args: `questions` (non-empty array). Each question needs `question` and non-empty `options`;
8
- each option needs `label` and may include `description`. Use `multiSelect=true` only when
9
- multiple choices are allowed.
10
-
11
- Returns standard JSON with `awaitUserResponse=true`, `metadata.kind="ask_user_question"`,
12
- and normalized questions.
File without changes