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.
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/PKG-INFO +2 -2
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/README.md +1 -1
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/pyproject.toml +1 -1
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/__init__.py +1 -1
- deepy_cli-0.1.11/src/deepy/data/tools/AskUserQuestion.md +18 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/system.py +1 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/agents.py +5 -2
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/ask_user_question.py +17 -1
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/message_view.py +85 -8
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/prompt_input.py +10 -4
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/terminal.py +84 -23
- deepy_cli-0.1.10/src/deepy/data/tools/AskUserQuestion.md +0 -12
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/cli.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/modify.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/errors.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/skills.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/status.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/builtin.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/usage.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.1.10 → deepy_cli-0.1.11}/src/deepy/utils/json.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -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
|
-
"
|
|
65
|
-
"to pause
|
|
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(
|
|
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}" + (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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] = [
|
|
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 = "
|
|
28
|
-
WINDOWS_PROMPT_TOOLBAR_HELP = "
|
|
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(
|
|
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.
|
|
446
|
-
self.
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
self.
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|