nanocode-cli 0.6.2__tar.gz → 0.6.3__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.
- {nanocode_cli-0.6.2/nanocode_cli.egg-info → nanocode_cli-0.6.3}/PKG-INFO +2 -1
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/README.md +1 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/README.zh-CN.md +1 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode.py +200 -11
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3/nanocode_cli.egg-info}/PKG-INFO +2 -1
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/pyproject.toml +1 -1
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/LICENSE +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/MANIFEST.in +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.6.2 → nanocode_cli-0.6.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanocode-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: A small terminal coding agent written in Python
|
|
5
5
|
Author-email: hit9 <hit9@icloud.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -121,6 +121,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
|
|
|
121
121
|
- Shell: `Bash`, `Git`.
|
|
122
122
|
- Tool results: `Recall`.
|
|
123
123
|
- Working notes: `Note`.
|
|
124
|
+
- Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
|
|
124
125
|
- MCP: `MCP` calls tools on configured MCP servers.
|
|
125
126
|
|
|
126
127
|
`Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
|
|
@@ -85,6 +85,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
|
|
|
85
85
|
- Shell: `Bash`, `Git`.
|
|
86
86
|
- Tool results: `Recall`.
|
|
87
87
|
- Working notes: `Note`.
|
|
88
|
+
- Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
|
|
88
89
|
- MCP: `MCP` calls tools on configured MCP servers.
|
|
89
90
|
|
|
90
91
|
`Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
|
|
@@ -85,6 +85,7 @@ nanocode
|
|
|
85
85
|
- Shell:`Bash`, `Git`。
|
|
86
86
|
- 工具结果:`Recall`。
|
|
87
87
|
- 工作笔记:`Note`。
|
|
88
|
+
- 询问用户:`Question` 在意图真正歧义时向用户提一个或多个问题(可选 choices、previews、recommended)。
|
|
88
89
|
- MCP:`MCP` 调用已配置 MCP 服务器上的工具。
|
|
89
90
|
|
|
90
91
|
`Read`、`Search` 和 `InspectCode` 会在合适时返回行锚点。`Edit` 使用当前 `line:hash` 锚点拒绝过期编辑。
|
|
@@ -59,7 +59,7 @@ from rich.console import Console
|
|
|
59
59
|
from rich.markdown import Markdown
|
|
60
60
|
from rich.rule import Rule
|
|
61
61
|
|
|
62
|
-
__version__ = "0.6.
|
|
62
|
+
__version__ = "0.6.3"
|
|
63
63
|
|
|
64
64
|
Json = dict[str, Any]
|
|
65
65
|
HTTP_USER_AGENT = "nanocode/" + __version__
|
|
@@ -81,6 +81,8 @@ CHAT_REASONING_EFFORT_VALUES: dict[str, dict[str, str | int]] = {
|
|
|
81
81
|
"enable_thinking": {"minimal": 256, "low": 1024, "medium": 4096, "high": 8192, "xhigh": 16384},
|
|
82
82
|
}
|
|
83
83
|
SELECTION_BACK = object()
|
|
84
|
+
SELECTION_FREE_TEXT = object()
|
|
85
|
+
DISMISSED = "(The user dismissed the question without answering.)"
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
class NanocodeError(Exception):
|
|
@@ -704,7 +706,7 @@ class SystemInfo:
|
|
|
704
706
|
class Session:
|
|
705
707
|
cwd: str = field(default_factory=os.getcwd)
|
|
706
708
|
system_info: SystemInfo | None = None
|
|
707
|
-
|
|
709
|
+
expected_git_branch: str = ""
|
|
708
710
|
config: Config = field(default_factory=Config)
|
|
709
711
|
settings: RuntimeSettings = field(default_factory=RuntimeSettings)
|
|
710
712
|
messages: list[Json] = field(default_factory=list)
|
|
@@ -722,8 +724,8 @@ class Session:
|
|
|
722
724
|
def __post_init__(self) -> None:
|
|
723
725
|
if self.system_info is None:
|
|
724
726
|
self.system_info = SystemInfo.detect(self.cwd)
|
|
725
|
-
if not self.
|
|
726
|
-
self.
|
|
727
|
+
if not self.expected_git_branch:
|
|
728
|
+
self.expected_git_branch = self.git_branch(self.cwd)
|
|
727
729
|
if self.mcp is None:
|
|
728
730
|
self.mcp = MCPManager(self)
|
|
729
731
|
|
|
@@ -2183,7 +2185,14 @@ class GitTool(Tool):
|
|
|
2183
2185
|
|
|
2184
2186
|
@classmethod
|
|
2185
2187
|
def payload_args(cls, payload: Json) -> list[Any]:
|
|
2186
|
-
argv =
|
|
2188
|
+
argv = payload.get("argv")
|
|
2189
|
+
if not isinstance(argv, list) or not argv:
|
|
2190
|
+
raise ToolError(
|
|
2191
|
+
"Git requires a non-empty 'argv' list. "
|
|
2192
|
+
'Signature: Git(argv=[command,...], cwd?) '
|
|
2193
|
+
'Example: {"argv":["status","--short"]}'
|
|
2194
|
+
)
|
|
2195
|
+
argv = [str(a) for a in argv]
|
|
2187
2196
|
return [("cwd=" + str(payload["cwd"])), *argv] if payload.get("cwd") else argv
|
|
2188
2197
|
|
|
2189
2198
|
def needs_confirmation(self) -> bool:
|
|
@@ -2198,6 +2207,11 @@ class GitTool(Tool):
|
|
|
2198
2207
|
self.validate_branch_safety(args, cwd)
|
|
2199
2208
|
try:
|
|
2200
2209
|
proc = subprocess.run([git, *args], cwd=cwd, text=True, capture_output=True, timeout=self.session.settings.shell_timeout)
|
|
2210
|
+
# When the user has nanocode switch branches, update expected_git_branch to the new
|
|
2211
|
+
# branch so later commits are not rejected. Gate on branch-changing commands only: an
|
|
2212
|
+
# unexpected (external) switch should still trip validate_branch_safety on commit.
|
|
2213
|
+
if proc.returncode == 0 and self.changes_branch(args):
|
|
2214
|
+
self.session.expected_git_branch = self.session.git_branch(cwd)
|
|
2201
2215
|
return self.process_result("GitToolResult", proc.returncode, proc.stdout, proc.stderr)
|
|
2202
2216
|
except subprocess.TimeoutExpired as error:
|
|
2203
2217
|
return self.process_result("GitToolResult", -1, error.stdout or "", (error.stderr or "") + "\ntimeout")
|
|
@@ -2227,9 +2241,9 @@ class GitTool(Tool):
|
|
|
2227
2241
|
if args[0] != "commit":
|
|
2228
2242
|
return
|
|
2229
2243
|
current = self.session.git_branch(cwd)
|
|
2230
|
-
|
|
2231
|
-
if
|
|
2232
|
-
raise ToolError(f"refusing git commit because branch changed from {
|
|
2244
|
+
expected = self.session.expected_git_branch
|
|
2245
|
+
if expected and current and current != expected:
|
|
2246
|
+
raise ToolError(f"refusing git commit because branch changed from {expected} to {current}")
|
|
2233
2247
|
if current in {"main", "master"} and self.session.settings.yolo:
|
|
2234
2248
|
raise ToolError(f"refusing git commit on {current} in yolo mode; explicit confirmation is required")
|
|
2235
2249
|
|
|
@@ -2425,6 +2439,118 @@ class NoteTool(Tool):
|
|
|
2425
2439
|
return ["\n".join(lines) or "{}"]
|
|
2426
2440
|
|
|
2427
2441
|
|
|
2442
|
+
@dataclass(frozen=True)
|
|
2443
|
+
class QuestionSpec:
|
|
2444
|
+
"""One validated question the model wants to ask the user."""
|
|
2445
|
+
|
|
2446
|
+
question: str
|
|
2447
|
+
choices: list[str] | None = None
|
|
2448
|
+
previews: list[str] | None = None
|
|
2449
|
+
recommended: int | None = None
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
class QuestionTool(Tool):
|
|
2453
|
+
NAME = "Question"
|
|
2454
|
+
DESCRIPTION = "Ask the user one or more questions (asked in sequence) and wait for their answers. Use when intent is genuinely ambiguous, a choice affects the codebase's external shape (module layout, public API, naming), or you need prioritization; prefer offering choices with previews, and optionally a recommended index when one option is clearly best. Do NOT ask about trivial internal details or anything determinable from context (Read/InspectCode/Bash) or already specified; if a reasonable default exists, proceed."
|
|
2455
|
+
SIGNATURE = 'Question(questions=[{question, choices?, previews?, recommended?}, ...])'
|
|
2456
|
+
EXAMPLE = (
|
|
2457
|
+
'One question, recommending a choice. Example: {"questions":[{"question":"Which approach?","choices":["Refactor","Rewrite"],"previews":["Extract module +87 -12","Rewrite from scratch"],"recommended":0}]}',
|
|
2458
|
+
'Batch related questions. Example: {"questions":[{"question":"Target runtime?","choices":["Node","Deno"]},{"question":"Name the module?"}]}',
|
|
2459
|
+
)
|
|
2460
|
+
MUTATES = False
|
|
2461
|
+
STORES_RESULT = True
|
|
2462
|
+
question_fn: Callable[[QuestionSpec, str], str] | None = None
|
|
2463
|
+
|
|
2464
|
+
@classmethod
|
|
2465
|
+
def params_schema(cls) -> Json:
|
|
2466
|
+
return {
|
|
2467
|
+
"type": "object",
|
|
2468
|
+
"properties": {
|
|
2469
|
+
"questions": {
|
|
2470
|
+
"type": "array",
|
|
2471
|
+
"minItems": 1,
|
|
2472
|
+
"description": "Questions to ask, one after another",
|
|
2473
|
+
"items": {
|
|
2474
|
+
"type": "object",
|
|
2475
|
+
"properties": {
|
|
2476
|
+
"question": {"type": "string", "description": "The question to ask the user"},
|
|
2477
|
+
"choices": {
|
|
2478
|
+
"type": "array",
|
|
2479
|
+
"items": {"type": "string"},
|
|
2480
|
+
"description": "Optional predefined choices the user can pick from",
|
|
2481
|
+
},
|
|
2482
|
+
"previews": {
|
|
2483
|
+
"type": "array",
|
|
2484
|
+
"items": {"type": "string"},
|
|
2485
|
+
"description": "Optional preview text per choice, shown as the user navigates",
|
|
2486
|
+
},
|
|
2487
|
+
"recommended": {
|
|
2488
|
+
"type": "integer",
|
|
2489
|
+
"minimum": 0,
|
|
2490
|
+
"description": "Optional 0-based index of the recommended choice; pre-selected and marked",
|
|
2491
|
+
},
|
|
2492
|
+
},
|
|
2493
|
+
"required": ["question"],
|
|
2494
|
+
"additionalProperties": False,
|
|
2495
|
+
},
|
|
2496
|
+
},
|
|
2497
|
+
},
|
|
2498
|
+
"required": ["questions"],
|
|
2499
|
+
"additionalProperties": False,
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
@classmethod
|
|
2503
|
+
def payload_args(cls, payload: Json) -> list[Any]:
|
|
2504
|
+
return [payload]
|
|
2505
|
+
|
|
2506
|
+
def call(self) -> str:
|
|
2507
|
+
if len(self.args) != 1 or not isinstance(self.args[0], dict):
|
|
2508
|
+
raise ToolError("Question requires named fields")
|
|
2509
|
+
questions = self.args[0].get("questions")
|
|
2510
|
+
if not isinstance(questions, list) or not questions:
|
|
2511
|
+
raise ToolError("Question requires a non-empty 'questions' list")
|
|
2512
|
+
# Validate the whole batch up front, so a malformed later question never strands the
|
|
2513
|
+
# user after they have already answered earlier ones.
|
|
2514
|
+
prepared: list[QuestionSpec] = []
|
|
2515
|
+
for item in questions:
|
|
2516
|
+
if not isinstance(item, dict):
|
|
2517
|
+
raise ToolError("each question must be an object with a 'question' field")
|
|
2518
|
+
question = str(item.get("question", "")).strip()
|
|
2519
|
+
if not question:
|
|
2520
|
+
raise ToolError("each question requires a 'question' field")
|
|
2521
|
+
choices = item.get("choices")
|
|
2522
|
+
previews = item.get("previews")
|
|
2523
|
+
recommended = item.get("recommended")
|
|
2524
|
+
if choices is not None:
|
|
2525
|
+
if not isinstance(choices, list) or not all(isinstance(c, str) for c in choices):
|
|
2526
|
+
raise ToolError("Question choices must be a list of strings")
|
|
2527
|
+
if previews is not None:
|
|
2528
|
+
if not isinstance(previews, list) or not all(isinstance(p, str) for p in previews):
|
|
2529
|
+
raise ToolError("Question previews must be a list of strings")
|
|
2530
|
+
if len(previews) != len(choices):
|
|
2531
|
+
raise ToolError("Question previews must match choices length")
|
|
2532
|
+
if recommended is not None and (
|
|
2533
|
+
isinstance(recommended, bool) or not isinstance(recommended, int) or not choices or not 0 <= recommended < len(choices)
|
|
2534
|
+
):
|
|
2535
|
+
raise ToolError("Question recommended must be a valid 0-based choice index")
|
|
2536
|
+
prepared.append(QuestionSpec(question, choices, previews, recommended))
|
|
2537
|
+
total = len(prepared)
|
|
2538
|
+
answers: list[tuple[str, str]] = []
|
|
2539
|
+
for index, spec in enumerate(prepared):
|
|
2540
|
+
position = f"{index + 1}/{total}" if total > 1 else ""
|
|
2541
|
+
answers.append((spec.question, self.question_fn(spec, position) if self.question_fn else spec.question))
|
|
2542
|
+
if len(answers) == 1:
|
|
2543
|
+
return answers[0][1]
|
|
2544
|
+
return "\n\n".join(f"Q: {q}\nA: {a}" for q, a in answers)
|
|
2545
|
+
|
|
2546
|
+
def short_args(self) -> list[str]:
|
|
2547
|
+
questions = self.args[0].get("questions") if self.args and isinstance(self.args[0], dict) else None
|
|
2548
|
+
if not isinstance(questions, list) or not questions:
|
|
2549
|
+
return [""]
|
|
2550
|
+
first = str((questions[0] or {}).get("question", "") or "").strip() if isinstance(questions[0], dict) else ""
|
|
2551
|
+
label = Tool.compact(first, 80)
|
|
2552
|
+
return [label + (f" (+{len(questions) - 1} more)" if len(questions) > 1 else "")]
|
|
2553
|
+
|
|
2428
2554
|
class MCPTool(Tool):
|
|
2429
2555
|
NAME = "MCP"
|
|
2430
2556
|
DESCRIPTION = "Call or describe external MCP server tools"
|
|
@@ -2525,6 +2651,7 @@ TOOLS: tuple[type[Tool], ...] = (
|
|
|
2525
2651
|
GitTool,
|
|
2526
2652
|
RecallTool,
|
|
2527
2653
|
NoteTool,
|
|
2654
|
+
QuestionTool,
|
|
2528
2655
|
)
|
|
2529
2656
|
TOOL_REGISTRY: dict[str, type[Tool]] = {tool.NAME: tool for tool in TOOLS}
|
|
2530
2657
|
|
|
@@ -3967,6 +4094,7 @@ class ToolRunner:
|
|
|
3967
4094
|
self.preview_full_fn: Callable[[str], None] | None = None
|
|
3968
4095
|
self.live_output: Callable[[str, str], None] | None = None
|
|
3969
4096
|
self.live_start: Callable[[], None] | None = None
|
|
4097
|
+
self.question_fn: Callable[[QuestionSpec, str], str] | None = None
|
|
3970
4098
|
|
|
3971
4099
|
def run(self, calls: list[ToolCall], batch_suffix: str = "") -> list[Json]:
|
|
3972
4100
|
messages = []
|
|
@@ -4026,6 +4154,8 @@ class ToolRunner:
|
|
|
4026
4154
|
if isinstance(tool, BashTool):
|
|
4027
4155
|
tool.live_output = self.live_output
|
|
4028
4156
|
started, approved, display = time.monotonic(), False, None
|
|
4157
|
+
if isinstance(tool, QuestionTool):
|
|
4158
|
+
tool.question_fn = self.question_fn
|
|
4029
4159
|
try:
|
|
4030
4160
|
display = self.short_call(call, tool.short_args())
|
|
4031
4161
|
if plan_error:
|
|
@@ -4634,11 +4764,12 @@ class Agent:
|
|
|
4634
4764
|
You are nanocode, a concise terminal coding agent.
|
|
4635
4765
|
|
|
4636
4766
|
TOOLS:
|
|
4637
|
-
- Available: Read LineCount List Find InspectCode Search Edit Bash Git Recall Note MCP.
|
|
4767
|
+
- Available: Read LineCount List Find InspectCode Search Edit Bash Git Recall Note Question MCP.
|
|
4638
4768
|
- Use exact tool names and named parameters; obey each tool DESCRIPTION/SIGNATURE.
|
|
4639
4769
|
- Files/code: Read/LineCount/List inspect files; Find/Search locate paths/text; InspectCode navigates symbols when available.
|
|
4640
4770
|
- Changes/commands: Edit writes files; Git handles git; Bash is fallback when built-ins do not fit.
|
|
4641
4771
|
- State/external: Recall retrieves tr.N outputs; Note maintains goal/plan/known/check; MCP calls configured external tools.
|
|
4772
|
+
- Restraint: Before calling "Question", make progress with other tools first; only ask when genuinely blocked, and batch related questions into one call.
|
|
4642
4773
|
|
|
4643
4774
|
FLOW:
|
|
4644
4775
|
- Act when clear; keep using tools until done, or return a final answer.
|
|
@@ -5347,7 +5478,7 @@ Mentions:
|
|
|
5347
5478
|
CLI:
|
|
5348
5479
|
--mcp "orion*,!orionEval" Select MCP servers by name glob; use all or none.
|
|
5349
5480
|
Tools:
|
|
5350
|
-
Read, LineCount, List, Find, InspectCode, Search, Edit, Bash, Git, Recall, Note, MCP.
|
|
5481
|
+
Read, LineCount, List, Find, InspectCode, Search, Edit, Bash, Git, Recall, Note, Question, MCP.
|
|
5351
5482
|
"""
|
|
5352
5483
|
|
|
5353
5484
|
def __init__(self, agent: Agent, input_fn=input, output_fn=print):
|
|
@@ -5386,6 +5517,7 @@ Tools:
|
|
|
5386
5517
|
self.agent.tools.preview_full_fn = lambda text: setattr(self, "approval_full_preview", text)
|
|
5387
5518
|
self.agent.tools.live_start = self.tool_live_start
|
|
5388
5519
|
self.agent.tools.live_output = self.tool_live_output
|
|
5520
|
+
self.agent.tools.question_fn = self.question_interaction
|
|
5389
5521
|
|
|
5390
5522
|
@staticmethod
|
|
5391
5523
|
def exit_app(app: Application) -> None:
|
|
@@ -5570,6 +5702,7 @@ Tools:
|
|
|
5570
5702
|
"choice.title": "ansicyan bold",
|
|
5571
5703
|
"choice.selected": "reverse",
|
|
5572
5704
|
"choice.disabled": "ansibrightblack",
|
|
5705
|
+
"choice.preview": "ansigreen italic",
|
|
5573
5706
|
"completion-menu": "noreverse bg:default",
|
|
5574
5707
|
"completion-menu.completion": "noreverse bg:default fg:ansiwhite",
|
|
5575
5708
|
"completion-menu.completion.current": "noreverse bg:default fg:ansicyan bold",
|
|
@@ -5975,7 +6108,14 @@ Tools:
|
|
|
5975
6108
|
labels: dict[str, str],
|
|
5976
6109
|
current: str,
|
|
5977
6110
|
disabled: set[str],
|
|
6111
|
+
*,
|
|
6112
|
+
preview_fn: Callable[[str], str] | None = None,
|
|
6113
|
+
free_text: bool = False,
|
|
5978
6114
|
) -> str | object | None:
|
|
6115
|
+
FREE_TEXT = "\x00free_text"
|
|
6116
|
+
if free_text and self.interactive_input:
|
|
6117
|
+
choices = (*choices, FREE_TEXT)
|
|
6118
|
+
labels = {**labels, FREE_TEXT: "Type freely..."}
|
|
5979
6119
|
state = {"query": "", "selected": 0, "search": False}
|
|
5980
6120
|
searching = Condition(lambda: bool(state["search"]))
|
|
5981
6121
|
|
|
@@ -6019,6 +6159,13 @@ Tools:
|
|
|
6019
6159
|
if selected:
|
|
6020
6160
|
parts.append(("[SetCursorPosition]", ""))
|
|
6021
6161
|
parts.append((style, ("> " if selected else " ") + f"{number:2d}. {label}\n"))
|
|
6162
|
+
if preview_fn and options:
|
|
6163
|
+
sel = int(state["selected"])
|
|
6164
|
+
preview_text = preview_fn(options[sel]) if 0 <= sel < len(options) else ""
|
|
6165
|
+
if preview_text:
|
|
6166
|
+
parts.append(("class:choice.disabled", " ──────────────────────────────────\n"))
|
|
6167
|
+
for line in preview_text.splitlines():
|
|
6168
|
+
parts.append(("class:choice.preview", " │ " + line + "\n"))
|
|
6022
6169
|
if state["search"]:
|
|
6023
6170
|
parts.append(("", "/" + query))
|
|
6024
6171
|
return parts
|
|
@@ -6070,7 +6217,8 @@ Tools:
|
|
|
6070
6217
|
return
|
|
6071
6218
|
options = enabled()
|
|
6072
6219
|
if options:
|
|
6073
|
-
|
|
6220
|
+
choice = options[int(state["selected"])]
|
|
6221
|
+
event.app.exit(result=SELECTION_FREE_TEXT if choice == FREE_TEXT else choice)
|
|
6074
6222
|
|
|
6075
6223
|
@bindings.add("c-c", eager=True)
|
|
6076
6224
|
@bindings.add("<sigint>", eager=True)
|
|
@@ -6105,6 +6253,47 @@ Tools:
|
|
|
6105
6253
|
app = self._make_app(Layout(HSplit([choice_window, self.status_window()]), focused_element=choice_window), bindings)
|
|
6106
6254
|
return self.run_input_app(app)
|
|
6107
6255
|
|
|
6256
|
+
def question_application(self, spec: QuestionSpec, position: str = "") -> str:
|
|
6257
|
+
"""Ask via the shared choice selector, with dynamic previews and a free-text fallback."""
|
|
6258
|
+
choices = spec.choices
|
|
6259
|
+
# Prefix the position (e.g. "(1/3) ...") into the question text so it renders as plain
|
|
6260
|
+
# markdown — no separate styled line, hence no ANSI escapes to mangle.
|
|
6261
|
+
prompt = f"({position}) {spec.question}" if position else spec.question
|
|
6262
|
+
if not choices or not self.interactive_input:
|
|
6263
|
+
return self.read_input(prompt)
|
|
6264
|
+
|
|
6265
|
+
if self.ui.color:
|
|
6266
|
+
self.ui.console.print(Markdown(prompt))
|
|
6267
|
+
else:
|
|
6268
|
+
self.emit(prompt + "\n")
|
|
6269
|
+
|
|
6270
|
+
# An optional recommended choice is pre-selected (via current) and marked (via labels),
|
|
6271
|
+
# reusing the selector's existing machinery.
|
|
6272
|
+
labels, current = {}, ""
|
|
6273
|
+
if spec.recommended is not None and 0 <= spec.recommended < len(choices):
|
|
6274
|
+
current = choices[spec.recommended]
|
|
6275
|
+
labels = {current: current + " (recommended)"}
|
|
6276
|
+
previews = spec.previews
|
|
6277
|
+
preview_map = {c: previews[i] for i, c in enumerate(choices) if previews and i < len(previews) and previews[i]}
|
|
6278
|
+
result = self.choice_application(
|
|
6279
|
+
"Select:", tuple(choices), labels, current, set(),
|
|
6280
|
+
preview_fn=lambda choice: preview_map.get(choice, ""),
|
|
6281
|
+
free_text=True,
|
|
6282
|
+
)
|
|
6283
|
+
if result is SELECTION_FREE_TEXT:
|
|
6284
|
+
return self.read_input(spec.question + " (type freely)")
|
|
6285
|
+
if isinstance(result, str):
|
|
6286
|
+
return result
|
|
6287
|
+
return DISMISSED # SELECTION_BACK (Esc) — user declined to answer
|
|
6288
|
+
|
|
6289
|
+
def question_interaction(self, spec: QuestionSpec, position: str = "") -> str:
|
|
6290
|
+
"""Entry point for Question tool — shows the chosen answer in CLI after selection."""
|
|
6291
|
+
result = self.question_application(spec, position)
|
|
6292
|
+
# Echo the picked choice (free-text/dismissal are already surfaced elsewhere).
|
|
6293
|
+
if spec.choices and result in spec.choices:
|
|
6294
|
+
self.emit(result + "\n")
|
|
6295
|
+
return result
|
|
6296
|
+
|
|
6108
6297
|
def select_model(self, choices: tuple[str, ...]) -> str | object | None:
|
|
6109
6298
|
current = self.session.config.provider.model
|
|
6110
6299
|
labels = {label: label for label in self.MODEL_LABELS if label in choices}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanocode-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: A small terminal coding agent written in Python
|
|
5
5
|
Author-email: hit9 <hit9@icloud.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -121,6 +121,7 @@ Interactive selectors support `j`/`k`, arrows, `/` search, Enter, and Esc. Input
|
|
|
121
121
|
- Shell: `Bash`, `Git`.
|
|
122
122
|
- Tool results: `Recall`.
|
|
123
123
|
- Working notes: `Note`.
|
|
124
|
+
- Ask the user: `Question` asks one or more questions (optional choices, previews, recommended) when intent is genuinely ambiguous.
|
|
124
125
|
- MCP: `MCP` calls tools on configured MCP servers.
|
|
125
126
|
|
|
126
127
|
`Read`, `Search`, and `InspectCode` return line anchors where useful. `Edit` uses current `line:hash` anchors to reject stale edits.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|