feat(issue-24): startup dialogs come from the agent adapter and are handled while waiting for the TUI

handle_startup_dialogs was a claude-only string ladder called only when
AGENT=claude, and wait_for_tui_ready does not treat a dialog screen as ready.
So agy, grok and codex sat on their directory-trust prompt for the full
readiness timeout, the session was recorded as tui-ready-timeout, and
onboarding injection was disabled (create_session.sh:313-314) - a human had to
accept each dialog by hand.

The signatures and accept keys move to the adapters as `startup_dialogs`
(pattern, keys, kind), published through the existing `facts` eval contract as
MAM_STARTUP_DIALOGS. claude's ladder moves over verbatim; agy takes Enter, grok
y, codex Enter, and codex's update notice takes 2 (Skip), which blocks a start
just as hard while changing nothing. kind is `trust` or `notice`:
--no-auto-trust leaves trust dialogs to a human and still answers notices,
since trusting a directory makes codex load its project-local config, hooks and
exec policy.

Re-sending is gated by time (MAM_DIALOG_RESEND_S, default 5s), not by frames.
#24's comment reports that `--source visible` keeps showing an accepted dialog
for a while, and _pane_capture hardcodes that source, so "still on screen" is
not evidence the keys were missed; acceptance is judged by the ready tokens
alone.

Two existing tests asserted the old shape and are updated, not weakened:
b19's "Escape branch stays" now looks for it in claude's adapter, and c1b's
"Enter once per iteration" becomes "never twice for one frame", which the
cooldown satisfies strictly more tightly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-22 04:22:09 +00:00
1 parent 3bcaa39ab0
commit 5ba04a27ca
11 files changed
+344 -29

No files matched your search

+106 -22
View File
@@ -2204,7 +2204,7 @@ wait_for_tui_ready() {
# handle it and do NOT declare ready on this iteration.
tail_lines=$(printf '%s\n' "$content" | grep -v '^[[:space:]]*$' | tail -n 20)
if printf '%s\n' "$tail_lines" | grep -E -q "$modal_pat" 2>/dev/null; then
handle_startup_dialogs "$sess" 1 || true
handle_startup_dialogs "$sess" 1 "$agent" || true
sleep 1
continue
fi
@@ -2254,7 +2254,7 @@ wait_for_tui_ready() {
fi
# 4. Startup dialog & hint resolution if not ready
handle_startup_dialogs "$sess" 1 || true
handle_startup_dialogs "$sess" 1 "$agent" || true
sleep 1
done
echo "⚠️ TUI readiness check timed out for '$sess'." >&2
@@ -2702,29 +2702,113 @@ send_keys_safe() {
return 4
}
# handle_startup_dialogs <sess> [timeout_sec=20]
# Post-start/resume dialog policy for claude: accept the trust / bypass
# dialogs ONLY when their signature is positively on screen; return as soon
# as the TUI banner is ready. Replaces the blind Enter/Down/Enter sequence.
# _mam_load_dialog_facts <agent>
# Populate MAM_STARTUP_DIALOGS for <agent>, once per agent per shell. Only that
# one fact is taken: `facts` is an eval contract (F8), and evaluating all of it
# here would clobber ready/placeholder tokens a caller deliberately set.
# An agent the adapters do not know keeps an empty list.
_mam_load_dialog_facts() {
local agent="$1" _f=""
if [ "${_MAM_DIALOGS_AGENT:-}" = "$agent" ]; then
return 0
fi
_f="$("$(_delegate_py_bin)" -m lib_py.agents facts "$agent" 2>/dev/null | sed -n 's/^MAM_STARTUP_DIALOGS=//p')" || _f=""
if [ -n "$_f" ]; then
eval "MAM_STARTUP_DIALOGS=$_f"
else
MAM_STARTUP_DIALOGS='[]'
fi
_MAM_DIALOGS_AGENT="$agent"
return 0
}
# _mam_dialog_pick — first actionable dialog on this frame.
# stdout: "send|<idx>|<key> <key>..." or "skip|<idx>|", nothing when there is
# none to act on. rc 1 means "nothing to do" (matched-but-cooling counts).
_mam_dialog_pick() {
MAM_DLG_PANE="$1" MAM_DLG_JSON="$2" MAM_DLG_TRUST="$3" \
MAM_DLG_STATE="$4" MAM_DLG_NOW="$5" MAM_DLG_COOLDOWN="$6" \
python3 -c '
import os, re, sys, json
pane = os.environ.get("MAM_DLG_PANE", "")
try:
dialogs = json.loads(os.environ.get("MAM_DLG_JSON") or "[]")
except Exception:
dialogs = []
auto_trust = os.environ.get("MAM_DLG_TRUST", "1") != "0"
try:
now = int(os.environ.get("MAM_DLG_NOW") or 0)
cooldown = int(os.environ.get("MAM_DLG_COOLDOWN") or 5)
except ValueError:
sys.exit(1)
state = {}
for tok in (os.environ.get("MAM_DLG_STATE") or "").split():
k, _, v = tok.partition(":")
try:
state[int(k)] = int(v)
except ValueError:
pass
for idx, entry in enumerate(dialogs):
try:
pattern, keys, kind = entry[0], entry[1], entry[2]
if not re.search(pattern, pane):
continue
except Exception:
continue
last = state.get(idx)
if last is not None and 0 <= now - last < cooldown:
sys.exit(1)
if kind == "trust" and not auto_trust:
print("skip|%d|" % idx)
sys.exit(0)
print("send|%d|%s" % (idx, " ".join(keys)))
sys.exit(0)
sys.exit(1)
'
}
# handle_startup_dialogs <sess> [timeout_sec=20] [agent=claude]
# Answer the blocking modals an agent shows before its TUI is usable. The
# signatures and accept keys come from the agent adapter (MAM_STARTUP_DIALOGS),
# so this is agent-agnostic - issue #24: it used to be a claude-only ladder
# called only for claude, and agy/grok/codex sat on their trust prompt until
# the readiness wait timed out and onboarding was skipped.
#
# `--source visible` keeps showing an accepted dialog for a while (#24
# comment), so a signature still on screen is NOT evidence that the keys were
# missed: re-sending for the same signature is gated by MAM_DIALOG_RESEND_S
# (default 5s) and acceptance is judged only by the ready tokens.
#
# MAM_AUTO_TRUST=0 leaves `trust` dialogs unanswered (warned once per
# cooldown); `notice` dialogs are always answered.
handle_startup_dialogs() {
local sess="$1" timeout="${2:-20}" waited=0 pane
local sess="$1" timeout="${2:-20}" agent="${3:-claude}"
local waited=0 pane pick action rest idx keys key now
local cooldown="${MAM_DIALOG_RESEND_S:-5}"
local auto_trust="${MAM_AUTO_TRUST:-1}"
_mam_load_dialog_facts "$agent"
local dialogs="${MAM_STARTUP_DIALOGS:-[]}"
local ready_re="${MAM_STRONG_READY_TOKENS:-${MAM_READY_TOKENS:-}}"
while [ "$waited" -lt "$timeout" ]; do
pane=$(_pane_tail "$sess" 20)
if printf '%s\n' "$pane" | grep -Eq 'Do you trust the files|Yes, I trust this folder|Quick safety check'; then
_sks_herdr send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -q 'Yes, try it'; then
# Fullscreen-renderer upsell modal (not the idle /tui tip). Enter would
# accept and restart the session without permission flags — reject.
_sks_herdr send-keys -t "$sess" Escape
elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then
_sks_herdr send-keys -t "$sess" Down
sleep 0.3
_sks_herdr send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -q 'Resuming the full session'; then
_sks_herdr send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -q 'Press Enter to continue'; then
_sks_herdr send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -Eq "${_MAM_READY_TOKENS_CLAUDE:-Anthropic|Assistant|Chat|Welcome}"; then
now=$(date +%s)
pick=""
pick=$(_mam_dialog_pick "$pane" "$dialogs" "$auto_trust" "${_MAM_DIALOG_SENT:-}" "$now" "$cooldown" 2>/dev/null || true)
if [ -n "$pick" ]; then
action="${pick%%|*}"
rest="${pick#*|}"
idx="${rest%%|*}"
keys="${rest#*|}"
if [ "$action" = "send" ]; then
for key in $keys; do
_sks_herdr send-keys -t "$sess" "$key" || true
sleep 0.3
done
else
echo "handle_startup_dialogs: a trust dialog is open on '$sess' and --no-auto-trust was given; accept it manually" >&2
fi
_MAM_DIALOG_SENT="${_MAM_DIALOG_SENT:-} $idx:$now"
elif [ -n "$ready_re" ] && printf '%s\n' "$pane" | grep -Eq "$ready_re" 2>/dev/null; then
return 0
fi
sleep 1