Skip to content

Commit bb6c1cf

Browse files
authored
fix missing notifications issue (#52)
1 parent b8ad3cc commit bb6c1cf

6 files changed

Lines changed: 171 additions & 4 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"name": "warp",
1111
"description": "Native Warp notifications when Claude completes tasks or needs input",
1212
"source": "./plugins/warp",
13-
"version": "2.0.0",
13+
"version": "2.1.0",
1414
"category": "productivity",
1515
"tags": ["notifications", "terminal", "warp"]
1616
}

plugins/warp/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "warp",
33
"description": "Warp terminal integration for Claude Code - native notifications, and more to come",
4-
"version": "2.0.0",
4+
"version": "2.1.0",
55
"author": {
66
"name": "Warp",
77
"url": "https://warp.dev"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/bin/bash
2+
# Emits an OSC terminal escape sequence using the best available method.
3+
#
4+
# Claude Code 2.1.141 added a `terminalSequence` JSON output field for hooks,
5+
# so they can deliver OSC sequences without a controlling terminal. Older
6+
# Claude Code doesn't know that field, and Stop hooks reject unknown fields
7+
# ("Stop hook error: JSON validation failed"), so on older versions we must
8+
# write to /dev/tty instead.
9+
#
10+
# Decision tree:
11+
# 1. CLAUDE_CODE_VERSION known, >= 2.1.141 → emit terminalSequence JSON
12+
# 2. CLAUDE_CODE_VERSION known, < 2.1.141 → write /dev/tty; give up if missing
13+
# 3. CLAUDE_CODE_VERSION unknown → try /dev/tty, fall back to JSON
14+
#
15+
# Usage:
16+
# source "$SCRIPT_DIR/emit-terminal-sequence.sh"
17+
# SEQ=$(printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY")
18+
# emit_terminal_sequence "$SEQ"
19+
#
20+
# When the sequence is delivered via /dev/tty (side effect), nothing is printed
21+
# to stdout. When it must go through terminalSequence, a JSON object is printed
22+
# to stdout — the caller should ensure this reaches the hook's stdout.
23+
24+
# The first Claude Code version that supports the terminalSequence output field.
25+
TERMINAL_SEQUENCE_MIN_VERSION="2.1.141"
26+
27+
# Compare two dotted version strings (e.g. "2.1.141" >= "2.1.141").
28+
# Returns 0 (true) if $1 >= $2, 1 (false) otherwise.
29+
_version_at_least() {
30+
local a b av bv i
31+
IFS=. read -ra a <<< "$1"
32+
IFS=. read -ra b <<< "$2"
33+
for ((i = 0; i < ${#b[@]}; i++)); do
34+
av="${a[i]:-0}"
35+
bv="${b[i]:-0}"
36+
if ((av > bv)); then return 0; fi
37+
if ((av < bv)); then return 1; fi
38+
done
39+
return 0
40+
}
41+
42+
# Extract a bare version number (e.g. "2.1.141") from `claude --version` output,
43+
# which may look like "claude 2.1.141" or "2.1.141" or "Claude Code v2.1.141".
44+
_parse_cc_version() {
45+
echo "$1" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
46+
}
47+
48+
# Returns 0 if the running Claude Code version supports terminalSequence.
49+
_supports_terminal_sequence() {
50+
local raw="${CLAUDE_CODE_VERSION:-}"
51+
[ -z "$raw" ] && return 1
52+
local ver
53+
ver=$(_parse_cc_version "$raw")
54+
[ -z "$ver" ] && return 1
55+
_version_at_least "$ver" "$TERMINAL_SEQUENCE_MIN_VERSION"
56+
}
57+
58+
emit_terminal_sequence() {
59+
local seq="$1"
60+
[ -z "$seq" ] && return 0
61+
62+
# Classify the running Claude Code version, if we can.
63+
local raw="${CLAUDE_CODE_VERSION:-}"
64+
local ver=""
65+
[ -n "$raw" ] && ver=$(_parse_cc_version "$raw")
66+
67+
if [ -n "$ver" ]; then
68+
if _version_at_least "$ver" "$TERMINAL_SEQUENCE_MIN_VERSION"; then
69+
# Known new Claude Code — use the structured output field.
70+
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
71+
else
72+
# Known-old Claude Code — /dev/tty is the only safe path.
73+
# Emitting terminalSequence here would be rejected by the Stop
74+
# hook validator as an unknown field.
75+
printf '%s' "$seq" > /dev/tty 2>/dev/null || true
76+
fi
77+
return 0
78+
fi
79+
80+
# Unknown Claude Code version — try /dev/tty, fall back to JSON
81+
# as a best-effort attempt for new CC without version detection.
82+
if printf '%s' "$seq" > /dev/tty 2>/dev/null; then
83+
return 0
84+
fi
85+
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
86+
}

plugins/warp/scripts/on-session-start.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ source "$SCRIPT_DIR/build-payload.sh"
2323
# Read hook input from stdin
2424
INPUT=$(cat)
2525

26+
# Best-effort Claude Code version detection.
27+
# Cache in $CLAUDE_ENV_FILE so subsequent hooks can skip the lookup, and
28+
# export it now so the rest of this hook (warp-notify.sh below) can use it.
29+
if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -z "${CLAUDE_CODE_VERSION:-}" ]; then
30+
CC_VERSION=$(claude --version 2>/dev/null | head -1 || true)
31+
if [ -n "$CC_VERSION" ]; then
32+
echo "export CLAUDE_CODE_VERSION=\"$CC_VERSION\"" >> "$CLAUDE_ENV_FILE"
33+
export CLAUDE_CODE_VERSION="$CC_VERSION"
34+
fi
35+
fi
36+
2637
# Read plugin version from plugin.json
2738
PLUGIN_VERSION=$(jq -r '.version // "unknown"' "$SCRIPT_DIR/../.claude-plugin/plugin.json" 2>/dev/null)
2839

plugins/warp/scripts/warp-notify.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
#
55
# For structured Warp notifications, title should be "warp://cli-agent"
66
# and body should be a JSON string matching the cli-agent notification schema.
7+
#
8+
# Output behavior:
9+
# - On old Claude Code: writes OSC 777 directly to /dev/tty (no stdout)
10+
# - On new Claude Code (>= 2.1.141): prints {terminalSequence: ...} JSON to
11+
# stdout so the caller can pass it through as hook output
712

813
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
914
source "$SCRIPT_DIR/should-use-structured.sh"
15+
source "$SCRIPT_DIR/emit-terminal-sequence.sh"
1016

1117
# Only emit notifications when we've confirmed the Warp build can render them.
1218
if ! should_use_structured; then
@@ -17,5 +23,5 @@ TITLE="${1:-Notification}"
1723
BODY="${2:-}"
1824

1925
# OSC 777 format: \033]777;notify;<title>;<body>\007
20-
# Write directly to /dev/tty to ensure it reaches the terminal
21-
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true
26+
SEQ=$(printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY")
27+
emit_terminal_sequence "$SEQ"

plugins/warp/tests/test-hooks.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,70 @@ assert_eq "newer preview version returns true" "0" "$?"
196196
unset WARP_CLI_AGENT_PROTOCOL_VERSION
197197
unset WARP_CLIENT_VERSION
198198

199+
echo ""
200+
echo "=== emit-terminal-sequence.sh ==="
201+
202+
source "$SCRIPT_DIR/../scripts/emit-terminal-sequence.sh"
203+
204+
echo ""
205+
echo "--- Version comparison ---"
206+
_version_at_least "2.1.141" "2.1.141"
207+
assert_eq "equal versions" "0" "$?"
208+
_version_at_least "2.1.142" "2.1.141"
209+
assert_eq "newer patch" "0" "$?"
210+
_version_at_least "2.2.0" "2.1.141"
211+
assert_eq "newer minor" "0" "$?"
212+
_version_at_least "3.0.0" "2.1.141"
213+
assert_eq "newer major" "0" "$?"
214+
_version_at_least "2.1.140" "2.1.141"
215+
assert_eq "older patch" "1" "$?"
216+
_version_at_least "2.0.999" "2.1.141"
217+
assert_eq "older minor" "1" "$?"
218+
_version_at_least "1.9.999" "2.1.141"
219+
assert_eq "older major" "1" "$?"
220+
221+
echo ""
222+
echo "--- Version parsing ---"
223+
assert_eq "bare version" "2.1.141" "$(_parse_cc_version '2.1.141')"
224+
assert_eq "prefixed with name" "2.1.141" "$(_parse_cc_version 'claude 2.1.141')"
225+
assert_eq "prefixed with v" "2.1.141" "$(_parse_cc_version 'Claude Code v2.1.141')"
226+
assert_eq "empty string" "" "$(_parse_cc_version '')"
227+
assert_eq "no version" "" "$(_parse_cc_version 'no version here')"
228+
229+
echo ""
230+
echo "--- _supports_terminal_sequence ---"
231+
232+
unset CLAUDE_CODE_VERSION
233+
_supports_terminal_sequence
234+
assert_eq "unset version → false" "1" "$?"
235+
236+
export CLAUDE_CODE_VERSION="2.1.141"
237+
_supports_terminal_sequence
238+
assert_eq "exact min version → true" "0" "$?"
239+
240+
export CLAUDE_CODE_VERSION="claude 2.1.150"
241+
_supports_terminal_sequence
242+
assert_eq "newer with prefix → true" "0" "$?"
243+
244+
export CLAUDE_CODE_VERSION="2.1.100"
245+
_supports_terminal_sequence
246+
assert_eq "older version → false" "1" "$?"
247+
248+
export CLAUDE_CODE_VERSION="garbage"
249+
_supports_terminal_sequence
250+
assert_eq "unparseable version → false" "1" "$?"
251+
252+
unset CLAUDE_CODE_VERSION
253+
254+
echo ""
255+
echo "--- emit_terminal_sequence output ---"
256+
257+
# With known new version → outputs terminalSequence JSON
258+
export CLAUDE_CODE_VERSION="2.1.141"
259+
OUTPUT=$(emit_terminal_sequence "test-seq")
260+
assert_json_field "new CC outputs terminalSequence" "$OUTPUT" ".terminalSequence" "test-seq"
261+
unset CLAUDE_CODE_VERSION
262+
199263
# --- Routing tests ---
200264
# These test the hook scripts as subprocesses to verify routing behavior.
201265
# We override /dev/tty writes since they'd fail in CI.

0 commit comments

Comments
 (0)