deepy-cli 0.1.9__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.9 → deepy_cli-0.1.11}/PKG-INFO +2 -2
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/README.md +1 -1
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/pyproject.toml +1 -1
- {deepy_cli-0.1.9 → 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.9 → deepy_cli-0.1.11}/src/deepy/data/tools/edit.md +4 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/modify.md +5 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/write.md +4 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/system.py +1 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/agents.py +5 -2
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/builtin.py +39 -3
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/ask_user_question.py +17 -1
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/message_view.py +85 -8
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/prompt_input.py +19 -132
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/terminal.py +84 -23
- deepy_cli-0.1.9/src/deepy/data/tools/AskUserQuestion.md +0 -12
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/cli.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/errors.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/skills.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/status.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/usage.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.1.9 → deepy_cli-0.1.11}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.1.9 → 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.
|
|
@@ -11,3 +11,7 @@ The file must be read first. Stale files are rejected. Repeated matches are reje
|
|
|
11
11
|
`replace_all` is true; candidate snippets can be reused with `snippet_id`. If exact text is
|
|
12
12
|
missing, simple over-escaping may be corrected or closest-match metadata returned. Success
|
|
13
13
|
includes diff metadata.
|
|
14
|
+
|
|
15
|
+
After repeated `old_string not found` failures, re-read the file. If you know the complete
|
|
16
|
+
intended content, switch to the managed whole-file replacement path instead of deleting
|
|
17
|
+
the file or recreating it through shell commands.
|
|
@@ -15,3 +15,8 @@ Args for existing files: `file_path`, `old_string`, `new_string`, optional
|
|
|
15
15
|
Existing files must be read before editing. Stale edits are rejected. Repeated matches
|
|
16
16
|
are rejected unless `replace_all` is true; candidate snippets can be reused with
|
|
17
17
|
`snippet_id`. Success includes diff metadata.
|
|
18
|
+
|
|
19
|
+
If several `old_string` attempts fail and you know the complete desired file content,
|
|
20
|
+
re-read the file and use the managed whole-file replacement path. Do not delete the file
|
|
21
|
+
and recreate it with shell commands or here-strings; that bypasses Deepy's encoding,
|
|
22
|
+
newline, and stale-write protections, especially on Windows.
|
|
@@ -10,3 +10,7 @@ Args: `path`, `content` (complete file content).
|
|
|
10
10
|
|
|
11
11
|
Existing files must be read first. If rejected for unread state, read and usually switch
|
|
12
12
|
to `edit` unless a full rewrite was requested. Stale writes are rejected. Success includes diff.
|
|
13
|
+
|
|
14
|
+
Use this managed path for intentional whole-file replacement when exact edit matching
|
|
15
|
+
keeps failing and the complete final content is known. Do not delete and recreate files
|
|
16
|
+
with shell commands or here-strings, especially for Unicode text on Windows.
|
|
@@ -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,
|
|
@@ -8,6 +8,7 @@ import re
|
|
|
8
8
|
import signal
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
|
+
import sys
|
|
11
12
|
import tempfile
|
|
12
13
|
import time
|
|
13
14
|
import urllib.parse
|
|
@@ -1161,6 +1162,7 @@ def _format_web_fetch_output(
|
|
|
1161
1162
|
class ToolRuntime:
|
|
1162
1163
|
cwd: Path
|
|
1163
1164
|
settings: Settings
|
|
1165
|
+
platform_name: str = field(default_factory=lambda: sys.platform)
|
|
1164
1166
|
file_state: FileState = field(default_factory=FileState)
|
|
1165
1167
|
running_processes: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
1166
1168
|
web_search_calls: int = 0
|
|
@@ -1305,13 +1307,22 @@ class ToolRuntime:
|
|
|
1305
1307
|
target = _resolve_in_cwd(self.cwd, path)
|
|
1306
1308
|
ok, error = self.file_state.check_writable(target, require_read=True)
|
|
1307
1309
|
if not ok:
|
|
1308
|
-
|
|
1310
|
+
metadata = _stale_write_recovery_metadata(target, error)
|
|
1311
|
+
return ToolResult.error_result(
|
|
1312
|
+
name,
|
|
1313
|
+
error or "File is not writable.",
|
|
1314
|
+
metadata=metadata,
|
|
1315
|
+
).to_json()
|
|
1309
1316
|
text_content, repair_metadata, content_error = _coerce_write_content(target, content)
|
|
1310
1317
|
if content_error is not None:
|
|
1311
1318
|
return ToolResult.error_result(name, content_error).to_json()
|
|
1312
1319
|
existing_metadata = _read_text_metadata(target) if target.exists() else None
|
|
1313
1320
|
old_content = existing_metadata.content if existing_metadata is not None else ""
|
|
1314
|
-
encoding =
|
|
1321
|
+
encoding = (
|
|
1322
|
+
existing_metadata.encoding
|
|
1323
|
+
if existing_metadata is not None
|
|
1324
|
+
else _default_new_text_encoding(text_content, platform_name=self.platform_name)
|
|
1325
|
+
)
|
|
1315
1326
|
line_endings = _detect_line_endings(old_content or text_content)
|
|
1316
1327
|
normalized_content = _normalize_line_endings(text_content, line_endings)
|
|
1317
1328
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1858,8 +1869,33 @@ def _python_text_encoding(encoding: str) -> str:
|
|
|
1858
1869
|
return "utf8"
|
|
1859
1870
|
|
|
1860
1871
|
|
|
1872
|
+
def _default_new_text_encoding(content: str, *, platform_name: str | None = None) -> str:
|
|
1873
|
+
resolved_platform = platform_name or sys.platform
|
|
1874
|
+
if resolved_platform.startswith("win") and _contains_non_ascii(content):
|
|
1875
|
+
return "utf8-sig"
|
|
1876
|
+
return "utf8"
|
|
1877
|
+
|
|
1878
|
+
|
|
1879
|
+
def _contains_non_ascii(text: str) -> bool:
|
|
1880
|
+
return any(ord(char) > 0x7F for char in text)
|
|
1881
|
+
|
|
1882
|
+
|
|
1861
1883
|
def _write_text_with_encoding(path: Path, content: str, encoding: str) -> None:
|
|
1862
|
-
path.
|
|
1884
|
+
path.write_bytes(content.encode(_python_text_encoding(encoding)))
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def _stale_write_recovery_metadata(path: Path, error: str | None) -> dict[str, object]:
|
|
1888
|
+
if error != "File changed since it was read: it no longer exists.":
|
|
1889
|
+
return {}
|
|
1890
|
+
return {
|
|
1891
|
+
"path": str(path),
|
|
1892
|
+
"recovery": (
|
|
1893
|
+
"The file was deleted after Deepy read it. Re-read the path or use a "
|
|
1894
|
+
"managed full-file replacement before deletion; do not recreate Unicode "
|
|
1895
|
+
"files through shell here-strings."
|
|
1896
|
+
),
|
|
1897
|
+
"recovery_kind": "stale_deleted_file",
|
|
1898
|
+
}
|
|
1863
1899
|
|
|
1864
1900
|
|
|
1865
1901
|
def _coerce_write_content(path: Path, content: object) -> tuple[str, dict[str, object], str | None]:
|
|
@@ -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,7 +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 = "
|
|
27
|
+
PROMPT_TOOLBAR_HELP = "Shift+Enter newline · Ctrl+D twice exit"
|
|
28
|
+
WINDOWS_PROMPT_TOOLBAR_HELP = "Ctrl+J newline · Ctrl+D twice exit"
|
|
28
29
|
PROMPT_MESSAGE: AnyFormattedText = [("class:prompt", "> ")]
|
|
29
30
|
PROMPT_PLACEHOLDER: AnyFormattedText = [("class:placeholder", "Type your message...")]
|
|
30
31
|
PROMPT_TOOLBAR: AnyFormattedText = [("class:toolbar.help", PROMPT_TOOLBAR_HELP)]
|
|
@@ -33,8 +34,6 @@ SHIFT_ENTER_SEQUENCES = (
|
|
|
33
34
|
"\x1b[27;2;13~", # xterm modified-key format.
|
|
34
35
|
"\x1b[13;2u", # Kitty/fixterms CSI-u format, used by modern terminals.
|
|
35
36
|
)
|
|
36
|
-
_WINDOWS_SHIFT_ENTER_PATCH_ATTR = "_deepy_shift_enter_patched"
|
|
37
|
-
_WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR = "_deepy_shift_enter_vt100_patched"
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
@dataclass(frozen=True)
|
|
@@ -111,138 +110,20 @@ def install_shift_enter_key_sequence_overrides() -> None:
|
|
|
111
110
|
prefix_cache = getattr(vt100_parser, "_IS_PREFIX_OF_LONGER_MATCH_CACHE", None)
|
|
112
111
|
if hasattr(prefix_cache, "clear"):
|
|
113
112
|
prefix_cache.clear()
|
|
114
|
-
install_windows_shift_enter_key_sequence_override()
|
|
115
113
|
|
|
116
114
|
|
|
117
|
-
def
|
|
118
|
-
*,
|
|
119
|
-
platform_name: str | None = None,
|
|
120
|
-
console_input_reader_cls: type | None = None,
|
|
121
|
-
vt100_console_input_reader_cls: type | None = None,
|
|
122
|
-
event_types: object | None = None,
|
|
123
|
-
key_event_record_cls: type | None = None,
|
|
124
|
-
) -> bool:
|
|
115
|
+
def is_windows_newline_fallback_enabled(platform_name: str | None = None) -> bool:
|
|
125
116
|
resolved_platform = platform_name or sys.platform
|
|
126
|
-
|
|
127
|
-
return False
|
|
128
|
-
if console_input_reader_cls is None and vt100_console_input_reader_cls is None:
|
|
129
|
-
try:
|
|
130
|
-
from prompt_toolkit.input import win32
|
|
131
|
-
except (AssertionError, ImportError):
|
|
132
|
-
return False
|
|
133
|
-
console_input_reader_cls = win32.ConsoleInputReader
|
|
134
|
-
vt100_console_input_reader_cls = getattr(win32, "Vt100ConsoleInputReader", None)
|
|
135
|
-
if event_types is None:
|
|
136
|
-
event_types = getattr(win32, "EventTypes", None)
|
|
137
|
-
if key_event_record_cls is None:
|
|
138
|
-
key_event_record_cls = getattr(win32, "KEY_EVENT_RECORD", None)
|
|
139
|
-
patched = False
|
|
140
|
-
if console_input_reader_cls is not None:
|
|
141
|
-
patched = _patch_windows_console_input_reader(console_input_reader_cls) or patched
|
|
142
|
-
if vt100_console_input_reader_cls is not None:
|
|
143
|
-
patched = (
|
|
144
|
-
_patch_windows_vt100_console_input_reader(
|
|
145
|
-
vt100_console_input_reader_cls,
|
|
146
|
-
event_types=event_types,
|
|
147
|
-
key_event_record_cls=key_event_record_cls,
|
|
148
|
-
)
|
|
149
|
-
or patched
|
|
150
|
-
)
|
|
151
|
-
return patched
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _patch_windows_console_input_reader(console_input_reader_cls: type) -> bool:
|
|
155
|
-
if getattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, False):
|
|
156
|
-
return True
|
|
157
|
-
|
|
158
|
-
from prompt_toolkit.key_binding.key_processor import KeyPress
|
|
159
|
-
|
|
160
|
-
original_handler = console_input_reader_cls._event_to_key_presses
|
|
161
|
-
shift_pressed = getattr(console_input_reader_cls, "SHIFT_PRESSED", 0x0010)
|
|
162
|
-
|
|
163
|
-
def patched_event_to_key_presses(self, ev):
|
|
164
|
-
key_presses = original_handler(self, ev)
|
|
165
|
-
if _is_windows_shift_enter_key_press(ev, key_presses, shift_pressed=shift_pressed):
|
|
166
|
-
return [KeyPress(Keys.Escape, ""), key_presses[0]]
|
|
167
|
-
return key_presses
|
|
168
|
-
|
|
169
|
-
setattr(
|
|
170
|
-
console_input_reader_cls,
|
|
171
|
-
"_deepy_original_event_to_key_presses",
|
|
172
|
-
original_handler,
|
|
173
|
-
)
|
|
174
|
-
console_input_reader_cls._event_to_key_presses = patched_event_to_key_presses
|
|
175
|
-
setattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, True)
|
|
176
|
-
return True
|
|
117
|
+
return resolved_platform.startswith("win")
|
|
177
118
|
|
|
178
119
|
|
|
179
|
-
def
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
) -> bool:
|
|
185
|
-
if event_types is None or key_event_record_cls is None:
|
|
186
|
-
return False
|
|
187
|
-
if getattr(vt100_console_input_reader_cls, _WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR, False):
|
|
188
|
-
return True
|
|
189
|
-
|
|
190
|
-
original_get_keys = vt100_console_input_reader_cls._get_keys
|
|
191
|
-
shift_pressed = getattr(vt100_console_input_reader_cls, "SHIFT_PRESSED", 0x0010)
|
|
192
|
-
|
|
193
|
-
def patched_get_keys(self, read, input_records):
|
|
194
|
-
for index in range(read.value):
|
|
195
|
-
input_record = input_records[index]
|
|
196
|
-
if input_record.EventType not in event_types:
|
|
197
|
-
continue
|
|
198
|
-
event_name = event_types[input_record.EventType]
|
|
199
|
-
event = getattr(input_record.Event, event_name)
|
|
200
|
-
if not isinstance(event, key_event_record_cls) or not event.KeyDown:
|
|
201
|
-
continue
|
|
202
|
-
if _is_windows_shift_enter_event(event, shift_pressed=shift_pressed):
|
|
203
|
-
yield SHIFT_ENTER_SEQUENCES[0]
|
|
204
|
-
continue
|
|
205
|
-
u_char = event.uChar.UnicodeChar
|
|
206
|
-
if u_char != "\x00":
|
|
207
|
-
yield u_char
|
|
208
|
-
|
|
209
|
-
setattr(
|
|
210
|
-
vt100_console_input_reader_cls,
|
|
211
|
-
"_deepy_original_get_keys",
|
|
212
|
-
original_get_keys,
|
|
120
|
+
def prompt_toolbar(platform_name: str | None = None) -> AnyFormattedText:
|
|
121
|
+
help_text = (
|
|
122
|
+
WINDOWS_PROMPT_TOOLBAR_HELP
|
|
123
|
+
if is_windows_newline_fallback_enabled(platform_name)
|
|
124
|
+
else PROMPT_TOOLBAR_HELP
|
|
213
125
|
)
|
|
214
|
-
|
|
215
|
-
setattr(vt100_console_input_reader_cls, _WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR, True)
|
|
216
|
-
return True
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _is_windows_shift_enter_key_press(
|
|
220
|
-
ev,
|
|
221
|
-
key_presses: list,
|
|
222
|
-
*,
|
|
223
|
-
shift_pressed: int,
|
|
224
|
-
) -> bool:
|
|
225
|
-
if not key_presses or len(key_presses) != 1:
|
|
226
|
-
return False
|
|
227
|
-
control_key_state = getattr(ev, "ControlKeyState", 0)
|
|
228
|
-
if not control_key_state & shift_pressed:
|
|
229
|
-
return False
|
|
230
|
-
key = getattr(key_presses[0], "key", None)
|
|
231
|
-
return key in {Keys.ControlM, Keys.ControlJ, Keys.Enter}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _is_windows_shift_enter_event(ev, *, shift_pressed: int) -> bool:
|
|
235
|
-
control_key_state = getattr(ev, "ControlKeyState", 0)
|
|
236
|
-
if not control_key_state & shift_pressed:
|
|
237
|
-
return False
|
|
238
|
-
u_char = getattr(getattr(ev, "uChar", None), "UnicodeChar", "")
|
|
239
|
-
virtual_key_code = getattr(ev, "VirtualKeyCode", None)
|
|
240
|
-
return u_char in {"\r", "\n"} or virtual_key_code == 13
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def is_windows_newline_fallback_enabled(platform_name: str | None = None) -> bool:
|
|
244
|
-
resolved_platform = platform_name or sys.platform
|
|
245
|
-
return resolved_platform.startswith("win")
|
|
126
|
+
return [("class:toolbar.help", help_text)]
|
|
246
127
|
|
|
247
128
|
|
|
248
129
|
def prompt_for_input(
|
|
@@ -254,15 +135,21 @@ def prompt_for_input(
|
|
|
254
135
|
return session.prompt(
|
|
255
136
|
prompt_message,
|
|
256
137
|
placeholder=PROMPT_PLACEHOLDER,
|
|
257
|
-
bottom_toolbar=
|
|
138
|
+
bottom_toolbar=prompt_toolbar() if bottom_toolbar is None else bottom_toolbar,
|
|
258
139
|
).strip()
|
|
259
140
|
|
|
260
141
|
|
|
261
|
-
def build_prompt_toolbar(
|
|
142
|
+
def build_prompt_toolbar(
|
|
143
|
+
context_status: str = "",
|
|
144
|
+
*,
|
|
145
|
+
platform_name: str | None = None,
|
|
146
|
+
) -> AnyFormattedText:
|
|
262
147
|
if not context_status:
|
|
263
|
-
return
|
|
148
|
+
return prompt_toolbar(platform_name)
|
|
264
149
|
return [
|
|
265
150
|
("class:toolbar.context", context_status),
|
|
151
|
+
("class:toolbar.separator", " · "),
|
|
152
|
+
*prompt_toolbar(platform_name),
|
|
266
153
|
]
|
|
267
154
|
|
|
268
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
|