2026-10-10 20:22:37 -04:00
# shellcheck shell=bash disable=SC2016 # single-quoted jq filters use jq variables
# Answer a session without switching to it (devdesk#22, #23), sourced by
# bin/devdesk and reached through `devdesk attach --answer|--peek|--reply`.
dd_state_file() {
printf '%s/%s.json\n' " $DEVDESK_STATUS_DIR " " $1 "
}
# The Claude window's visible screen (not the scrollback).
dd_claude_screen() {
dd_tmux capture-pane -p -J -t "= $1 :claude" 2>/dev/null
}
# --answer SESSION SINCE allow|deny: press the permission prompt's key in that
# session's Claude window. Only when the same wait is still on (SINCE) and the
# prompt is actually on screen, so a stale button never types into Claude.
cmd_answer() {
[ $# -eq 3 ] || dd_die "usage: devdesk attach --answer SESSION SINCE allow|deny"
local session = $1 since = $2 choice = $3 file
dd_valid_name " $session " || dd_die "invalid session name: $session "
[[ $since = ~ ^[ 0-9] +$ ]] || dd_die "bad wait id: $since "
case $choice in allow | deny) ;; *) dd_die "answer allow or deny" ;; esac
if ! { dd_server_up && dd_tmux has-session -t "= $session " 2>/dev/null; } ; then dd_die "session ' $session ' isn't running" ; fi
file = $( dd_state_file " $session " )
if [ " $( jq -rM '"\(.state) \(.since) \(.request != null)"' " $file " 2>/dev/null) " != "waiting $since true" ] ; then
dd_die "nothing to answer: it was already answered"
fi
# Every permission prompt lists "1. Yes" first (Bash, edits, fetches, MCP).
if ! dd_claude_screen " $session " | tail -n 40 | grep -Eq '^[[:space:]]*(❯ [[:space:]]*)?1\.[[:space:]]+Yes' ; then
dd_die "the permission prompt isn't on screen any more"
fi
if [ " $choice " = allow ] ; then
dd_tmux send-keys -t "= $session :claude" 1
else
dd_tmux send-keys -t "= $session :claude" Escape
fi
# A denial doesn't fire a hook; the turn ends there. An approval is followed
# by PostToolUse, which takes it from here.
local tmp
tmp = $( mktemp " $DEVDESK_STATUS_DIR /. $session .XXXXXX" ) || return 0
if jq -cMn --arg s " $( [ " $choice " = allow ] && echo working || echo idle) " --arg m " $( [ " $choice " = allow ] && echo "Allowed from DevDesk" || echo "Denied from DevDesk" ) " \
'{state: $s, since: (now | floor), event: "DevDeskAnswer", message: $m}' >" $tmp " ; then
mv -f " $tmp " " $file "
else
rm -f " $tmp "
fi
}
# The session's conversation file, from the id the SessionStart hook recorded.
dd_transcript() {
local id
id = $( dd_registry_get " $1 " claude_session_id)
2026-10-11 01:01:51 -04:00
dd_convo_file " $id "
2026-10-10 20:22:37 -04:00
}
# --peek SESSION: what the session is doing and the last exchange, from the end
# of its transcript (the last assistant text and the last thing you asked).
cmd_peek() {
[ $# -eq 1 ] || dd_die "usage: devdesk attach --peek SESSION"
2026-10-10 20:37:46 -04:00
local session = $1 transcript status commands
2026-10-10 20:22:37 -04:00
dd_valid_name " $session " || dd_die "invalid session name: $session "
status = $( jq -cM . " $( dd_state_file " $session " ) " 2>/dev/null) || status = null
2026-10-10 20:37:46 -04:00
# The last commands this session ran (devdesk#27).
commands = $( tail -n 8 " $DEVDESK_STATE_DIR /commands/ $session .jsonl" 2>/dev/null | jq -cMs '.' 2>/dev/null) || commands = '[]'
[ -n " $commands " ] || commands = '[]'
2026-10-10 20:22:37 -04:00
transcript = $( dd_transcript " $session " ) || transcript = ""
if [ -z " $transcript " ] || [ ! -r " $transcript " ] ; then
2026-10-10 20:37:46 -04:00
jq -cMn --arg s " $session " --argjson st " ${ status :- null } " --argjson c " $commands " '{session: $s, status: $st, assistant: null, user: null, commands: $c}'
2026-10-10 20:22:37 -04:00
return
fi
2026-10-10 20:37:46 -04:00
tail -n 400 -- " $transcript " | jq -cMs --arg s " $session " --argjson st " ${ status :- null } " --argjson c " $commands " '
2026-10-10 20:22:37 -04:00
def text: if type == "string" then . else ([.[]? | select(.type == "text") | .text] | join("\n")) end;
[.[] | select(.isSidechain != true and .isMeta != true)] as $all
| ([$all[] | select(.type == "assistant") | {text: (.message.content | text), at: .timestamp} | select(.text != "")] | last) as $a
| ([$all[] | select(.type == "user" and (.message.content | type == "string" or (any(.[]?; .type == "text"))))
| {text: (.message.content | text), at: .timestamp}
| select(.text != "" and (.text | startswith("<") | not))] | last) as $u
| {session: $s, status: $st,
assistant: (if $a then {text: ($a.text | .[-4000:]), at: $a.at, truncated: ($a.text | length > 4000)} else null end),
2026-10-10 20:37:46 -04:00
user: (if $u then {text: ($u.text | .[-600:]), at: $u.at} else null end), commands: $c}' 2>/dev/null ||
jq -cMn --arg s " $session " --argjson st " ${ status :- null } " --argjson c " $commands " '{session: $s, status: $st, assistant: null, user: null, commands: $c}'
2026-10-10 20:22:37 -04:00
}
# --reply SESSION BYTES: type a message into the session's Claude prompt and
# send it. The text arrives as base64 after the READY line (like uploads) and is
# pasted as one bracketed paste, so newlines in it don't send it early.
cmd_reply() {
[ $# -eq 2 ] || dd_die "usage: devdesk attach --reply SESSION BYTES"
local session = $1 size = $2 file tmp b64len
dd_valid_name " $session " || dd_die "invalid session name: $session "
if ! [[ $size = ~ ^[ 0-9] +$ ]] || [ " $size " -lt 1 ] || [ " $size " -gt 16384 ] ; then dd_die "a reply is 1 to 16384 bytes" ; fi
if ! { dd_server_up && dd_tmux has-session -t "= $session " 2>/dev/null; } ; then dd_die "session ' $session ' isn't running" ; fi
file = $( dd_state_file " $session " )
if [ " $( jq -rM '"\(.state) \(.request != null)"' " $file " 2>/dev/null) " = "waiting true" ] ; then
dd_die "Claude is asking for a permission: answer that first"
fi
tmp = $( mktemp)
trap 'rm -f "$tmp"' HUP INT TERM
# shellcheck disable=SC2017 # integer ceil(size/3)*4: the base64 length, on purpose
b64len = $(( ( size + 2 ) / 3 * 4 ))
stty raw -echo 2>/dev/null || true
printf 'DEVDESK-REPLY-READY\r\n'
head -c " $b64len " | base64 -d >" $tmp " 2>/dev/null || true
stty sane 2>/dev/null || true
if [ " $( stat -c %s " $tmp " ) " != " $size " ] ; then
rm -f " $tmp "
dd_die "reply incomplete"
fi
dd_tmux load-buffer -b devdesk-reply " $tmp " && dd_tmux paste-buffer -p -d -b devdesk-reply -t "= $session :claude" &&
sleep 0.2 && dd_tmux send-keys -t "= $session :claude" Enter
rm -f " $tmp "
trap - HUP INT TERM
}