docs: describe prompt notifications

A prompt that waits on the human is announced after a grace period, and the
notification can carry the answer: allow / deny / review buttons for a
confirmation, a reply field for an `ask_user_line`. `doc/notifications.md` lays
out the two channels (the optional `desktop-notifier` extra, and OSC 777 / OSC 9
written to `/dev/tty` with no dependency), what each prompt kind offers, why the
reason line stays out by default, and the platform limits — no buttons where a
server does not render them, a reply field only under KDE, kitty left with the
bell.

The example config and the README config block gain the `[notify]` section, and
AGENTS.md documents the three prompt hooks and the rule that only the outermost
prompt of a nested read is announced.
This commit is contained in:
2026-10-21 20:03:36 +08:00
parent be8a0111ec
commit b197c98ce9
5 changed files with 131 additions and 2 deletions
+6 -2
View File
@@ -32,8 +32,10 @@
``@tool`` / tags / registry. History stays chat-completions-shaped.
- tools: Workspace file/process/VCS/chat/todo/net/skill tools + directory-access
grants (`access`); catalog; plugins; instance/session context and views.
- prompting: Shared ask/choose/form for CLI and tools.
- cli: Click entry, slash registry, REPL, one-shot chat.
- prompting: Shared ask/choose/form for CLI and tools; the hooks a host uses
to take over a prompt (output break, SIGINT guard, waiting notification).
- cli: Click entry, slash registry, REPL, one-shot chat; `notify` announces a
prompt that waits on the human.
- web: Web service (Phase H; not implemented).
Plugins (third-party entry points, allowlisted under ``[plugins]``): [plugins.md](./plugins.md).
@@ -41,4 +43,6 @@ Conversation units (a *round* = one request→response plus its tool batch, a
*turn* = a user message to the explicit stop): [turns-and-rounds.md](./turns-and-rounds.md).
Images (a turn's pictures, a tool result's pictures, what each wire surface
takes): [images.md](./images.md).
Prompt notifications (a prompt that waits on the human, and how it can be
answered from the notification): [notifications.md](./notifications.md).
+91
View File
@@ -0,0 +1,91 @@
# Prompt notifications
A turn stops while a prompt waits on the human. If you switched to another
window, the wait is invisible until you come back — so the CLI announces it.
A prompt is announced only after `grace_seconds` of waiting (5s by default), so
answering at the terminal stays silent. Bursts are throttled by
`min_interval_seconds`, and the announcement says what is being asked, not what
it would run: the one-line reason can name a command, argv or URL, and a
notification outlives the prompt (and may be mirrored to a lock screen), so it
travels only with `include_reason = true`.
## Configuration
```toml
[notify]
enabled = true
grace_seconds = 5.0
min_interval_seconds = 10.0
channels = ["auto"] # auto | desktop | terminal
actions = true # offer the answer, not just the news
include_reason = false
protocol = "" # terminal protocol override: osc777 / osc9 / bell
```
`channels = ["auto"]` tries the desktop first and falls back to the terminal;
`["desktop"]` and `["terminal"]` pin one of them. Notifications are only sent
from an interactive run — a one-shot `plyngent "…"` never waits on a human, and
a prompt answered inside the grace period is never announced at all.
## What is answered where
| Prompt | Notification offers |
|---|---|
| tool confirm (single / a round's batch) | Allow once / Deny / Go to terminal |
| command denylist, `fetch_local` private host | same three |
| directory-access request, skill write | same three |
| `ask_user_line` | a reply field — the answer *is* the text you type |
| `ask_user_choice`, `ask_user_form`, PTY secrets | nothing (terminal only) |
| limit-continue | nothing (terminal only) |
An answer read from the notification is treated exactly like a typed one —
`allow` is the `y` the prompt accepts, `deny` is `n` — and it never records a
session grant: the button means "this once". "Go to terminal" withdraws the
notification and leaves the prompt waiting. A secret is never asked for in a
notification, and never echoed into one.
## Desktop channel
The desktop channel needs the optional extra:
```console
pip install 'plyngent[notify]'
```
It wraps `desktop-notifier`, which talks to `org.freedesktop.Notifications` on
Linux, Notification Center on macOS and WinRT on Windows. Buttons need a
notification server that renders them (KDE Plasma and GNOME Shell do; a bare
`dunst` may not, and then the notification is text-only without any error). The
reply field is KDE-only in practice: it is the `inline-reply` capability plus the
`x-kde-reply-submit-button-text` hint, which only KNotifications implements.
Notifications are shown from the library's async API on a loop that runs in a
daemon thread, because callbacks only fire while the loop that owns the
notification is running.
## Terminal channel
No dependency and no extra: the CLI writes an escape sequence to `/dev/tty`
(never stdout, so a redirect cannot swallow it) and the terminal turns it into a
native notification.
| Protocol | Sequence | Terminals |
|---|---|---|
| OSC 777 | `ESC ] 777 ; notify ; title ; body` | Ghostty, WezTerm, Warp, Contour, VTE (GNOME Terminal, Tilix, Terminator), rxvt, Windows Terminal |
| OSC 9 | `ESC ] 9 ; title: body` | iTerm2, Apple Terminal, ConEmu |
| bell | `BEL` | everything else |
Detection reads the environment only (`TERM_PROGRAM`, `TERM`, `WT_SESSION`,
`VTE_VERSION`), so it never blocks and never touches the input stream;
`protocol` pins it. Inside tmux the sequence is wrapped in DCS passthrough —
tmux needs `set -g allow-passthrough on` — and inside GNU Screen in a Screen
DCS. kitty speaks OSC 99 only, which is not implemented, so it keeps the bell;
install the extra to reach it through the desktop instead.
## Failure is always silent
A missing extra, a desktop without a notification server, a refused write: each
falls through to the next channel and never raises into the prompt. There is no
sound, no remote push and no notification when a turn merely ends — only when a
prompt is waiting for you.
+21
View File
@@ -42,6 +42,27 @@
# include_project_roots = true
# allow_write = false
# Notifications while a prompt waits on the human: a tool confirmation, a
# directory-access grant, a skill write, an ``ask_user_*`` question, or the
# limit-continue prompt. A prompt is announced only after `grace_seconds` of
# waiting, so one you answer at the terminal stays silent; `min_interval_seconds`
# throttles bursts.
# channels: "auto" tries the desktop first (needs the `notify` extra:
# `pip install plyngent[notify]`) and falls back to a terminal escape sequence;
# "desktop" / "terminal" pin one of them.
# `actions` offers allow / deny / review as notification buttons; where the
# platform has none the notification stays text-only.
# `include_reason` adds the one-line reason — it can name a command or a URL, so
# it stays off by default (a notification may be mirrored to a lock screen).
# [notify]
# enabled = true
# grace_seconds = 5.0
# min_interval_seconds = 10.0
# channels = ["auto"]
# actions = true
# include_reason = false
# protocol = "" # terminal protocol: "" = detect, or osc777 / osc9 / bell
# MCP (Model Context Protocol) servers exposed as agent tools.
# Defining a server opts it in; tools appear as mcp__<server>__<tool> with
# LOCAL tags (confirm flow applies). Names in disable stay disconnected.