From 959f0d46cac1a09d0821da599d8dff54d1147892 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Fri, 30 Oct 2026 18:59:43 +0200 Subject: mark forgotten drafts in pi agents' input boxes, and keep them across a crash or restore --- tests/e2e.sh | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- tests/pi-live.sh | 23 +++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/e2e.sh b/tests/e2e.sh index 7f72e3c..e73dc73 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -285,13 +285,15 @@ cat >"$DIR/ext-harness.mjs" <<'JS' // ("agent_start"), runs commands ("/importance high"), and asks for argument // completions ("complete:importance h"). Custom session entries persist in // $SESSION_ENTRIES, so a later run reads them back as the session; what the -// extension shows and emits goes to $EXT_LOG. +// extension shows and emits goes to $EXT_LOG. The input box holds +// $EDITOR_TEXT; the session file is $SESSION_FILE. import fs from "node:fs"; const ext = await import(process.env.EXTENSION); const on = {}, commands = {}; const store = process.env.SESSION_ENTRIES; const entries = store && fs.existsSync(store) ? JSON.parse(fs.readFileSync(store, "utf8")) : []; const log = (line) => process.env.EXT_LOG && fs.appendFileSync(process.env.EXT_LOG, line + "\n"); +let editor = process.env.EDITOR_TEXT ?? ""; ext.default({ on: (name, fn) => (on[name] = fn), registerCommand: (name, command) => (commands[name] = command), @@ -304,8 +306,13 @@ ext.default({ const ctx = { mode: "tui", isIdle: () => true, - sessionManager: { getEntries: () => entries, getSessionFile: () => undefined }, - ui: { setStatus: (key, text) => log(`status ${key} ${text ?? ""}`), notify: (text) => log(`notify ${text}`) }, + sessionManager: { getEntries: () => entries, getSessionFile: () => process.env.SESSION_FILE }, + ui: { + setStatus: (key, text) => log(`status ${key} ${text ?? ""}`), + notify: (text) => log(`notify ${text}`), + getEditorText: () => editor, + setEditorText: (text) => { editor = text; log(`editor ${text}`); }, + }, }; for (const arg of process.argv.slice(2)) { if (arg.startsWith("/")) { @@ -1032,6 +1039,66 @@ expect "the menu: only those by hand, not clickable" "mb --print | grep '^⟳'" "$(printf '⟳ %s Agents Need /reload\ttheir dingger extension is too old to reload them\t(disabled)' "$BY_HAND")" t kill-session -t ext +# --- 24. forgotten drafts ----------------------------------------------------- +# pi's extension reports the draft in its input box; dingger marks it where +# the user is not looking, and keeps it in a file per session that comes back +# with the session. (Typing into a real pi: tests/pi-live.sh.) +echo "24. forgotten drafts" +draft_report() { # pane draft + python3 - "$HOOK" "$1" "$2" <<'PY' +import json, socket, sys +params = {"pane_id": sys.argv[2], "agent": "pi", "draft": sys.argv[3]} +s = socket.socket(socket.AF_UNIX) +s.connect(sys.argv[1]) +s.sendall((json.dumps({"method": "pane.report_agent", "params": params}) + "\n").encode()) +s.close() +PY +} +t new-session -d -s dr -n d1 -c "$DIR" "$DIR/bin/pi" +t new-window -d -t dr: -n d2 -c "$DIR" "$DIR/bin/pi" +t new-window -d -t dr: -n away -c "$DIR" "sleep 100000" +D1=$(t display -p -t dr:d1 '#{pane_id}'); D2=$(t display -p -t dr:d2 '#{pane_id}') +look "$(t display -p -t dr:away '#{pane_id}')" +for p in "$D1" "$D2"; do report "$p" working ""; report "$p" idle ""; expect "dr: $p done" "opt $p @agent_state" done; done +look "$D1"; sleep 1; look "$D2"; sleep 1; look "$(t display -p -t dr:away '#{pane_id}')" +expect "dr: both idle" "opt $D1 @agent_state; opt $D2 @agent_state" "idle +idle" +draft_report "$D2" "$(printf 'fix the\n sidecar;')" +expect "a draft, as one line, its trailing ; kept" "opt $D2 @dingger_draft" "fix the sidecar;" +expect "pane options after it still written" "opt $D2 @agent_state" idle +drow() { mb --print | grep -F "dr · $1 ·" | cut -f2; } +expect "menu: marked ✎" "drow d2" "✎ pi" +expect "first among idle agents" "mb --print | grep -F 'dr · d' | cut -f3 | cut -d' ' -f3 | tr '\n' ' '" "d2 d1 " +expect "search finds it by its draft" "mb --print-search sidecar | head -1 | cut -f3" "dr · d2 · pi" +expect "dingger status marks it" "$DINGGER status | grep -F 'dr · d2' | grep -c '✎ '" 1 +look "$D2" +expect "where you look, no mark: you may be typing it" "drow d2" "pi" +expect "nor in dingger status" "$DINGGER status | grep -F 'dr · d2' | grep -c '✎ '" 0 +look "$D1" +draft_report "$D2" "" +expect "a cleared draft: gone" "opt $D2 @dingger_draft" "" +expect "menu: no mark" "drow d2" "pi" +draft_report "$D2" ";" +expect "a draft of just ;" "opt $D2 @dingger_draft" ";" +report "$D2" working "" +expect "a report without a draft keeps it" "opt $D2 @dingger_draft" ";" +if command -v node >/dev/null; then + DRAFTS="$(dirname "$HOOK")/drafts" + EDITOR_TEXT="half a thought" SESSION_FILE="$DIR/sessions/s1.jsonl" iext "$D1" session_start + expect "the extension reports its draft" "opt $D1 @dingger_draft" "half a thought" + expect "and keeps it for the session" "cat $DRAFTS/s1.jsonl.draft" "half a thought" + : >"$EXTLOG"; SESSION_FILE="$DIR/sessions/s1.jsonl" iext "$D1" session_start + expect "the session again: its draft back in the input box" "grep '^editor' $EXTLOG" "editor half a thought" + expect "and reported" "opt $D1 @dingger_draft" "half a thought" + : >"$EXTLOG"; EDITOR_TEXT="typed meanwhile" SESSION_FILE="$DIR/sessions/s1.jsonl" iext "$D1" session_start + expect "an input box with text keeps it" "grep -c '^editor' $EXTLOG" 0 + expect "and that is the draft now" "cat $DRAFTS/s1.jsonl.draft" "typed meanwhile" + : >"$EXTLOG"; SESSION_FILE="$DIR/sessions/s2.jsonl" iext "$D1" session_start + expect "another session: none of it" "grep -c '^editor' $EXTLOG" 0 + expect "dr: no draft reported" "opt $D1 @dingger_draft" "" +fi +t kill-session -t dr + # Every daemon this run started must stop with it. kill "$DAEMON_PID" "$DAEMON2_PID" 2>/dev/null expect "the test daemons stop" "ps -o pid= -p $DAEMON_PID,$DAEMON2_PID | wc -l | tr -d ' '" 0 diff --git a/tests/pi-live.sh b/tests/pi-live.sh index 4ea4caf..6dff42f 100755 --- a/tests/pi-live.sh +++ b/tests/pi-live.sh @@ -169,6 +169,10 @@ expect "and the turn ends" "st agent" done 80 t send-keys -t main:agent -l "half typed" sleep 0.5 expect "a draft: it refuses" "ask_reload" '{"ok":false,"reason":"draft"}' +draft() { t show -pv -t main:agent @dingger_draft 2>/dev/null; } +draft_file() { echo "$D/drafts/$(basename "$(t show -pv -t main:agent @agent_session)").draft"; } +expect "the draft reaches dingger" "draft" "half typed" 30 +expect "and is kept for the session" "cat \"\$(draft_file)\"" "half typed" 30 # Through dingger: refused for the draft, retried until it can. "$DINGGER" reload-agents >"$D/reload.out" expect "reload-agents: not yet" "head -1 $D/reload.out" "reloaded 0 of 1 agent on an old dingger extension" @@ -176,6 +180,8 @@ expect "it says why it waits" "sed -n 2p $D/reload.out" "the other 1 reload once expect "the daemon was told no: a draft" "sed 's/\x1b\[[0-9;]*m//g' $D/daemon.log | grep -c 'reload: not now target=main:agent reason=draft'" 1 30 expect "the draft is untouched" "t capture-pane -p -t main:agent | grep -c 'half typed'" 1 for _ in $(seq 10); do t send-keys -t main:agent BSpace; done +expect "deleted: no draft" "draft" "" 30 +expect "nor its file" "[ -e \"\$(draft_file)\" ] && echo left || echo gone" gone 30 expect "once the input box is empty: reloaded" "t show -pv -t main:agent @dingger_extension 2>/dev/null" "$CURRENT" 150 expect "by the daemon" "reloads reloading" 1 30 expect "a new instance (a new socket)" "[ \"\$(ctl)\" != \"$SOCK1\" ] && [ -S \"\$(ctl)\" ] && echo yes" yes @@ -183,9 +189,13 @@ expect "the old socket is gone" "[ -e \"$SOCK1\" ] && echo left || echo gone" go expect "nothing left to reload" "t show -gv @dingger_outdated 2>/dev/null" 0 30 expect "still done (unseen)" "st agent" done expect "no message reached the model" "grep -c dingger-reload \"\$(t show -pv -t main:agent @agent_session)\"" 0 -type_line agent "slow 1 after" +t send-keys -t main:agent -l "slow 1 after" +expect "typed, then a pause: a draft" "draft" "slow 1 after" 30 +t send-keys -t main:agent Enter expect "it still works: working" "st agent" working 50 expect "and done" "st agent" done 60 +expect "a message sent is no draft" "draft" "" 30 +expect "nor kept" "[ -e \"\$(draft_file)\" ] && echo left || echo gone" gone 30 echo "2. screen detection (pi without the extension)" t send-keys -t main:plain "PI_CODING_AGENT_DIR=$D/agent-plain pi $NO_MCP" Enter @@ -200,6 +210,10 @@ echo "3. restore" SESSION=$(t show -pv -t main:agent @agent_session) latest() { ls "$D"/state/snapshots/*.json 2>/dev/null | tail -1; } expect "the snapshot has the agent's session" "grep -c \"\\\"session\\\": \\\"$SESSION\\\"\" \"\$(latest)\" 2>/dev/null" 1 80 +# A draft left behind when tmux goes away. +# (`\;`: send-keys would end its command at a trailing ;) +t send-keys -t main:agent -l 'an unsent idea\;' +expect "a draft before the crash" "t show -pv -t main:agent @dingger_draft 2>/dev/null" "an unsent idea;" 30 t kill-server expect "the layout becomes the restore candidate" "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"status\"])' $D/state/restore.json 2>/dev/null" offered 80 "$DINGGER" restore >"$D/restore.out" 2>&1 @@ -208,10 +222,17 @@ expect "resumed 1 of 1 agents" "grep -c '^resumed 1 of 1 agents' $D/restore.out" expect "it runs again, reporting the same session" "t show -pv -t main:agent @agent_session 2>/dev/null" "$SESSION" 150 expect "with its conversation" "t capture-pane -p -t main:agent -S - | grep -c 'slow 3 hello'" 1 50 expect "and its importance" "t show -pv -t main:agent @dingger_importance 2>/dev/null" high 50 +expect "and its draft, back in the input box" "t capture-pane -p -t main:agent | grep -c '^an unsent idea;'" 1 50 +expect "reported again" "t show -pv -t main:agent @dingger_draft 2>/dev/null" "an unsent idea;" 30 echo "4. exit" +LAST_DRAFT=$(draft_file) +expect "its draft is kept" "cat $LAST_DRAFT" "an unsent idea;" +# Deleted, then quit at once: before the input box is read again. +for _ in $(seq 20); do t send-keys -t main:agent BSpace; done t send-keys -t main:agent C-d expect "the agent is gone from the pane" "t show -pv -t main:agent @agent 2>/dev/null" "" 100 +expect "a draft deleted before quitting is not kept" "[ -e $LAST_DRAFT ] && echo left || echo gone" gone 30 echo echo "$PASSES passed, $FAILS failed" -- cgit v1.2.3