Resolve daemon session window via job-name to window-title match
Replaces the focus-based fallback (which mis-attributed under fleet view) with a deterministic session_id -> job name -> kitty window title mapping.
This commit is contained in:
+44
-45
@@ -42,12 +42,9 @@ mkdir -p "$STATE_DIR"
|
||||
# transcript_path). Keep stdin drained so claude doesn't stall.
|
||||
STDIN_JSON=$(cat 2>/dev/null || echo '{}')
|
||||
|
||||
# session_id from the hook payload — stable per session. Used to
|
||||
# cache the resolved address (resolution is the expensive step) and,
|
||||
# under the daemon, to locate the per-session rendezvous socket.
|
||||
# session_id from the hook payload — stable per session, used to map
|
||||
# the session to its kitty window under the daemon.
|
||||
SID=$(printf '%s' "$STDIN_JSON" | jq -r '.session_id // empty' 2>/dev/null || true)
|
||||
CACHE_FILE=""
|
||||
[ -n "$SID" ] && CACHE_FILE="$STATE_DIR/.session-$SID.addr"
|
||||
|
||||
# Walk a PPid chain upward from $1 until an ancestor pid owns a
|
||||
# Hyprland client (the kitty window). Prints the bare (0x-stripped)
|
||||
@@ -69,54 +66,56 @@ addr_from_ppid_chain() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Daemon mode: the session runs in a pre-warmed spare process with no
|
||||
# terminal in its process tree or environment — KITTY_* are unset and
|
||||
# the PPid chain leads to the daemon -> systemd, never to kitty. The
|
||||
# daemon brokers all I/O (every rv/pty socket peers to the daemon, not
|
||||
# to the front-end; it writes no session metadata to disk and the
|
||||
# transcript is not held open), so there is NO traversable link from
|
||||
# the hook back to the originating window. The only signal available
|
||||
# from inside the session is Hyprland's global focus.
|
||||
#
|
||||
# We exploit timing: the FIRST hook of a session is UserPromptSubmit,
|
||||
# which fires the instant you submit your first prompt — while the
|
||||
# kitty window you typed in is still focused. Capture the focused
|
||||
# window then and cache it (resolve_addr caches by session id, so
|
||||
# every later hook reuses this even after focus moves). Guarded to
|
||||
# kitty windows so a non-terminal focus never gets mispainted.
|
||||
addr_active_window() {
|
||||
local j class addr
|
||||
j=$(hyprctl -j activewindow 2>/dev/null || true)
|
||||
class=$(printf '%s' "$j" | jq -r '.class // empty' 2>/dev/null || true)
|
||||
case "$class" in
|
||||
*kitty*|*Kitty*) : ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
addr=$(printf '%s' "$j" | jq -r '.address // empty' 2>/dev/null || true)
|
||||
# Daemon mode: the session runs in a pre-warmed spare with no terminal
|
||||
# in its process tree or environment (KITTY_* unset, PPid chain leads
|
||||
# to the daemon -> systemd). The daemon brokers all I/O, so there is
|
||||
# no socket/env/PPid path back to the originating window. But it does
|
||||
# launch each session as a NAMED background job, and the viewer writes
|
||||
# that name into the kitty window's title. So map session_id -> job
|
||||
# name -> the kitty window whose title contains it. This is focus-
|
||||
# independent, so it works under fleet view where the session's window
|
||||
# is never focused.
|
||||
session_name() {
|
||||
local sid=$1 name=""
|
||||
local jobf="$HOME/.claude/jobs/${sid%%-*}/state.json"
|
||||
if [ -f "$jobf" ]; then
|
||||
name=$(jq -r --arg s "$sid" \
|
||||
'select(.sessionId==$s) | .name // empty' "$jobf" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$name" ]; then
|
||||
name=$(jq -rs --arg s "$sid" \
|
||||
'map(select(.sessionId==$s)) | .[0].name // empty' \
|
||||
"$HOME"/.claude/sessions/*.json 2>/dev/null || true)
|
||||
fi
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
addr_via_session_name() {
|
||||
local sid=$1
|
||||
[ -n "$sid" ] || return 1
|
||||
local name
|
||||
name=$(session_name "$sid")
|
||||
[ -n "$name" ] || return 1
|
||||
local addr
|
||||
addr=$(hyprctl -j clients 2>/dev/null \
|
||||
| jq -r --arg n "$name" \
|
||||
'.[] | select(((.class // "")|ascii_downcase|contains("kitty"))
|
||||
and ((.title // "")|contains($n))) | .address' \
|
||||
| head -n1)
|
||||
addr=${addr#0x}
|
||||
[ -n "$addr" ] || return 1
|
||||
printf '%s' "$addr"
|
||||
}
|
||||
|
||||
# Resolve once, cache by session id, reuse for every later hook.
|
||||
# Resolve fresh on every hook (cheap; no caching). Name-matching is
|
||||
# deterministic and tracks the session even if its window moves.
|
||||
resolve_addr() {
|
||||
if [ -n "$CACHE_FILE" ] && [ -s "$CACHE_FILE" ]; then
|
||||
cat "$CACHE_FILE"
|
||||
return 0
|
||||
fi
|
||||
local addr=""
|
||||
addr=$(addr_from_ppid_chain $$) || addr="" # non-daemon: real chain
|
||||
# Daemon fallback uses the focused window, so it's only meaningful
|
||||
# for a session that actually owns a terminal. A background job
|
||||
# (CLAUDE_JOB_DIR set) has no window of its own — letting it grab
|
||||
# the focused window paints a random unrelated kitty. Skip it.
|
||||
if [ -z "$addr" ] && [ -z "${CLAUDE_JOB_DIR:-}" ]; then
|
||||
addr=$(addr_active_window) || addr="" # daemon: focus
|
||||
fi
|
||||
if [ -z "$addr" ]; then return 1; fi
|
||||
if [ -n "$CACHE_FILE" ]; then
|
||||
printf '%s\n' "$addr" >"$CACHE_FILE" 2>/dev/null || true
|
||||
if [ -z "$addr" ]; then
|
||||
addr=$(addr_via_session_name "$SID") || addr="" # daemon: name->title
|
||||
fi
|
||||
[ -n "$addr" ] || return 1
|
||||
printf '%s' "$addr"
|
||||
}
|
||||
|
||||
@@ -264,7 +263,7 @@ case "$EVENT" in
|
||||
;;
|
||||
SessionEnd)
|
||||
rm -f "$STATE_FILE" "$TOOLS_FILE" "$TOOLS_FILE.lock" \
|
||||
"$STATE_DIR/.$ADDR.ping" ${CACHE_FILE:+"$CACHE_FILE"}
|
||||
"$STATE_DIR/.$ADDR.ping"
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user