diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh index 7d46ed2..9726d88 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -22,7 +22,7 @@ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) [ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}' # Build a human-readable summary -TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path else (tostring | .[0:80]) end) // ""' 2>/dev/null) +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path elif .questions then (.questions[0].question // "") else (tostring | .[0:80]) end) // ""' 2>/dev/null) SUMMARY="Wants to run $TOOL_NAME" if [ -n "$TOOL_PREVIEW" ]; then if [ ${#TOOL_PREVIEW} -gt 120 ]; then diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index 4163bb9..80f00f7 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -36,18 +36,32 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then # containing {type:"text"} blocks. Tool-result messages have content arrays # containing only {type:"tool_result"} blocks. We filter to messages that # have at least one "text" block (or are a plain string). + # "user" entries are not only what the human typed. Background-task + # notifications, blocking-hook feedback, system reminders and messages from + # other sessions arrive under the same type, and taking the last one made + # notifications read "'\n 0 then . - else empty - end - ] | last | - if .message.content | type == "array" - then [.message.content[] | select(.type == "text") | .text] | join(" ") - else .message.content // empty - end + def text: (if .message.content | type == "string" + then .message.content + else ([.message.content[]? | select(.type == "text") | .text] | join(" ")) + end) + | gsub("\\[Image[^\\]]*\\]"; "") + | gsub("^\\s+|\\s+$"; ""); + def synthetic: startswith("<") # , , + # , + or startswith("Stop hook feedback:") + or startswith("Another Claude session sent a message") + or startswith("[Request interrupted") + or startswith("Caveat:"); + [ .[] + | select(.type == "user") + | select(has("isSidechain") and .isSidechain | not) + | text + | select(. != null and . != "") + | select(synthetic | not) + ] | last // "" ' "$TRANSCRIPT_PATH" 2>/dev/null) # Get the last assistant response diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 754bdd0..cd84837 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -266,6 +266,74 @@ unset CLAUDE_CODE_VERSION HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)" +echo "" +echo "=== Stop query selection ===" + +# Feeds a transcript through on-stop.sh and pulls .query back out, so the +# synthetic-entry filtering is covered by the suite. +stop_query() { + local file + file=$(mktemp) + printf '%s\n' "$@" > "$file" + echo "{\"session_id\":\"s\",\"cwd\":\"/tmp/p\",\"transcript_path\":\"$file\"}" | + WARP_CLI_AGENT_PROTOCOL_VERSION=1 \ + WARP_CLIENT_VERSION="v9999.99.99.99.99.stable_99" \ + CLAUDE_CODE_VERSION="9999.0.0" \ + bash "$HOOK_DIR/on-stop.sh" 2>/dev/null | + jq -r '.terminalSequence // empty' | + sed 's/^.*warp:\/\/cli-agent;//' | tr -d '\007' | + jq -r '.query // empty' + rm -f "$file" +} + +HUMAN='{"type":"user","message":{"role":"user","content":"ship the release"}}' +NOTIF='{"type":"user","message":{"role":"user","content":"\nbuild done"}}' +HOOKFB='{"type":"user","message":{"role":"user","content":"Stop hook feedback:\n[hook] do X"}}' +PEER='{"type":"user","message":{"role":"user","content":"Another Claude session sent a message:\nhi"}}' +IMGONLY='{"type":"user","message":{"role":"user","content":"[Image: source: /tmp/a.png]"}}' +SIDE='{"type":"user","isSidechain":true,"message":{"role":"user","content":"subagent prompt"}}' +REPLY='{"type":"assistant","message":{"content":[{"type":"text","text":"done"}]}}' + +echo "" +echo "--- Query skips synthetic user entries ---" + +assert_eq "plain prompt is used" "ship the release" "$(stop_query "$HUMAN" "$REPLY")" +assert_eq "task notification skipped" "ship the release" "$(stop_query "$HUMAN" "$REPLY" "$NOTIF")" +assert_eq "stop hook feedback skipped" "ship the release" "$(stop_query "$HUMAN" "$REPLY" "$HOOKFB")" +assert_eq "peer message skipped" "ship the release" "$(stop_query "$HUMAN" "$REPLY" "$PEER")" +assert_eq "subagent prompt skipped" "ship the release" "$(stop_query "$HUMAN" "$REPLY" "$SIDE")" +assert_eq "image-only message leaves no path" "ship the release" "$(stop_query "$HUMAN" "$REPLY" "$IMGONLY")" + +echo "" +echo "=== Permission request preview ===" + +# Runs on-permission-request.sh end to end and pulls .summary back out of the +# OSC sequence, so the jq preview expression is covered by the suite. +permission_summary() { + echo "$1" | WARP_CLI_AGENT_PROTOCOL_VERSION=1 \ + WARP_CLIENT_VERSION="v9999.99.99.99.99.stable_99" \ + CLAUDE_CODE_VERSION="9999.0.0" \ + bash "$HOOK_DIR/on-permission-request.sh" 2>/dev/null | + jq -r '.terminalSequence // empty' | + sed 's/^.*warp:\/\/cli-agent;//' | tr -d '\007' | + jq -r '.summary // empty' +} + +echo "" +echo "--- Tool preview ---" + +assert_eq "Bash shows the command" "Wants to run Bash: git push origin main" \ + "$(permission_summary '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}')" + +assert_eq "Write shows the path" "Wants to run Write: /tmp/a.txt" \ + "$(permission_summary '{"tool_name":"Write","tool_input":{"file_path":"/tmp/a.txt","content":"hi"}}')" + +assert_eq "AskUserQuestion shows the question" "Wants to run AskUserQuestion: Deploy now or wait?" \ + "$(permission_summary '{"tool_name":"AskUserQuestion","tool_input":{"questions":[{"question":"Deploy now or wait?","header":"Deploy"}]}}')" + +assert_eq "unknown tool falls back to the raw input" 'Wants to run WebFetch: {"url":"https://example.com"}' \ + "$(permission_summary '{"tool_name":"WebFetch","tool_input":{"url":"https://example.com"}}')" + echo "" echo "=== Routing ==="