core/prompting: answer a question with a notification reply

`ask` now reads its answer from the wait's source as well, which is what makes
the notification's reply field mean something: the reply typed into it is the
answer the caller gets. The readline path stays whenever the source offers no
answers, so line editing and Tab completion only go away when the human asked
for a surface that can answer — and a terminal answer still wins over one that
arrives through the notification.

The read itself is the poll-based one this module already had, so the same
"no deadline, wait for a line or an answer" rule and the same Ctrl+C handling
apply; a cancelled read raises `NonInteractiveError`, as the backend's own
cancellation does.
This commit is contained in:
2026-10-21 20:02:56 +08:00
parent 35595d8b04
commit be8a0111ec
2 changed files with 63 additions and 2 deletions
+31 -2
View File
@@ -509,6 +509,34 @@ def _show_choices(
backend.echo(" (or type a custom answer)")
def _read_with_source(
prompt: str,
source: PromptAnswerSource | None,
*,
default: str | None,
completions: Sequence[str] | None = None,
) -> str:
"""Read one answer line, also accepting one offered by *source*.
The backend's readline path is kept whenever nothing can answer remotely, so
line editing (and Tab completion) only go away when the human asked for a
notification that carries the answer.
"""
backend = get_prompt_backend()
if source is None:
return backend.read_line(prompt, default=default, completions=completions).strip()
try:
answer = source.take() if not sys.stdin.isatty() else read_line_with_timeout(f"{prompt}: ", None, source=source)
except OSError, ValueError, AttributeError:
answer = source.take()
if answer is None:
if default is not None:
return default
msg = "prompt cancelled"
raise NonInteractiveError(msg)
return answer.strip()
def ask(
prompt: str,
*,
@@ -523,9 +551,10 @@ def ask(
if not backend.is_interactive() and default is None:
msg = f"non-interactive: cannot prompt for {prompt!r}"
raise NonInteractiveError(msg)
with prompt_wait("ask", prompt):
with prompt_wait("ask", prompt) as wait:
backend.secho(prompt, fg="yellow")
return backend.read_line("Answer", default=default, completions=completions).strip()
source = wait if wait is not None and wait.supports_actions else None
return _read_with_source("Answer", source, default=default, completions=completions)
def ask_secret(prompt: str) -> str: