core/cli: raise the wait signal from every confirm prompt

Every place the CLI blocks on the human now announces itself: the single and
batched tool confirms, the command-denylist and `fetch_local` policies, a skill
write, a directory-access request, the `ask_user_*` questions and the
limit-continue prompt. `_setup_hooks` installs the notifier from `[notify]`, and
only for an interactive run — a one-shot run never waits on a human.

An answer that came back through the notification is read as if it had been
typed: `allow` / `deny` map onto the `y` / `n` the prompts already accept, and
the confirmation prompts switch from click's readline to the poll-based read
only when the notification actually offered answers — otherwise the terminal
experience is untouched. "Go to terminal" never reaches the prompt at all.

The stdin read behind the timed prompts now watches in 100ms slices instead of
one full-length `select`, which is what lets a notification answer end it; an
answer still loses to a human typing at the terminal. A `None` timeout waits
for whichever arrives first, and a non-pollable stdin still gives up at once.

`prompt_wait` announces only the outermost prompt of a nested read: a confirm
that asks a follow-up question while it is already on screen would otherwise
buzz the human a second time.
This commit is contained in:
2026-10-21 20:01:38 +08:00
parent 036e0b40ad
commit 35595d8b04
5 changed files with 318 additions and 66 deletions
+4 -4
View File
@@ -313,13 +313,13 @@ def test_prompt_skill_write_confirm_answers(monkeypatch: pytest.MonkeyPatch) ->
target = Path("/skills/acme")
with temporary_backend(ScriptedBackend([])):
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout: "o\n")
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout, **_kwargs: "o\n")
once = limits.prompt_skill_write_confirm(target, "create", "create skill", 30.0)
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout: "s\n")
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout, **_kwargs: "s\n")
session = limits.prompt_skill_write_confirm(target, "create", "create skill", 30.0)
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout: "n\n")
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout, **_kwargs: "n\n")
denied = limits.prompt_skill_write_confirm(target, "create", "create skill", 30.0)
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout: None)
monkeypatch.setattr(limits, "_read_yes_no_line_with_timeout", lambda _timeout, **_kwargs: None)
timed_out = limits.prompt_skill_write_confirm(target, "create", "create skill", 30.0)
assert (once.allow, once.session) == (True, False)
assert (session.allow, session.session) == (True, True)