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:
Oleks
2026-06-24 15:05:51 +03:00
parent 4771aa7fec
commit a8d97a2453
+44 -45
View File
@@ -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