copilot-notify 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/bin/bash
  2. # Wrapper script for copilot to get clickable focus notifications via swaync
  3. set -euo pipefail
  4. notify_focus() {
  5. local action="$1"
  6. local win_addr="$2"
  7. [ -n "$action" ] || return 0
  8. [ "$action" = "f" ] && [ -n "$win_addr" ] && hyprctl dispatch focuswindow address:"$win_addr" >/dev/null 2>&1 || true
  9. }
  10. notify_action_required() {
  11. local win_addr="$1"
  12. local action
  13. action=$(notify-send "Copilot" "✋ Action required" --action="f=Focus" -u critical -a "Copilot" -i ~/dl/copilot.png || true)
  14. notify_focus "$action" "$win_addr"
  15. }
  16. notify_task_finished() {
  17. local win_addr="$1"
  18. local action
  19. action=$(notify-send "Copilot" "✅ Task finished" --action="f=Focus" -u normal -a "Copilot" -i ~/dl/copilot.png || true)
  20. notify_focus "$action" "$win_addr"
  21. }
  22. start_interactive_monitor() {
  23. local win_addr="$1"
  24. local started_epoch="$2"
  25. local monitor_pid="$BASHPID"
  26. local log_file
  27. local line
  28. local busy=0
  29. local ask_user_pending=0
  30. local permission_pending=0
  31. local permission_notified=0
  32. local permission_timer_pid=""
  33. local tool_calls_count=0
  34. clear_permission_state() {
  35. permission_pending=0
  36. permission_notified=0
  37. if [ -n "${permission_timer_pid:-}" ]; then
  38. kill "$permission_timer_pid" 2>/dev/null || true
  39. permission_timer_pid=""
  40. fi
  41. }
  42. start_permission_timer() {
  43. if [ -n "${permission_timer_pid:-}" ]; then
  44. kill "$permission_timer_pid" 2>/dev/null || true
  45. permission_timer_pid=""
  46. fi
  47. (
  48. sleep 1
  49. kill -USR1 "$monitor_pid" 2>/dev/null || true
  50. ) &
  51. permission_timer_pid=$!
  52. }
  53. trap '
  54. if [ "$permission_pending" -eq 1 ] && [ "$permission_notified" -eq 0 ]; then
  55. notify_action_required "$win_addr"
  56. permission_notified=1
  57. fi
  58. ' USR1
  59. # Wait for the process log created by this copilot invocation.
  60. for _ in $(seq 1 60); do
  61. log_file=$(find "$HOME/.copilot/logs" -maxdepth 1 -name 'process-*.log' -newermt "@$started_epoch" -print 2>/dev/null | sort | tail -n1 || true)
  62. if [ -n "${log_file:-}" ]; then
  63. break
  64. fi
  65. sleep 0.1
  66. done
  67. [ -n "${log_file:-}" ] || return 0
  68. while IFS= read -r line; do
  69. if [ "$ask_user_pending" -gt 0 ]; then
  70. ask_user_pending=$((ask_user_pending - 1))
  71. fi
  72. case "$line" in
  73. *"Tool calls count: "*)
  74. tool_calls_count="${line##*Tool calls count: }"
  75. tool_calls_count="${tool_calls_count%%[^0-9]*}"
  76. [ -n "$tool_calls_count" ] || tool_calls_count=0
  77. ;;
  78. *"kind: assistant_turn_start"*)
  79. busy=1
  80. clear_permission_state
  81. ;;
  82. *'"name": "ask_user"'*)
  83. # "ask_user" appears in tool schema on every turn; only alert if a
  84. # real tool call follows immediately with a question payload.
  85. ask_user_pending=8
  86. ;;
  87. *'"arguments": "{\"question\":'*)
  88. if [ "$ask_user_pending" -gt 0 ]; then
  89. notify_action_required "$win_addr"
  90. ask_user_pending=0
  91. fi
  92. ;;
  93. *"Permission request (kind="*"): routing via PermissionService"*)
  94. # PermissionService handles all tool calls; only notify if the
  95. # request stays unresolved for a moment (likely a real prompt).
  96. permission_pending=1
  97. permission_notified=0
  98. start_permission_timer
  99. ask_user_pending=0
  100. ;;
  101. *"respondToPermission:"*)
  102. clear_permission_state
  103. ask_user_pending=0
  104. ;;
  105. *"kind: tool_call_executed"*)
  106. # Single-tool turns that auto-approve complete quickly; suppress
  107. # stale timer notifications in that common case.
  108. if [ "$permission_pending" -eq 1 ] && [ "$tool_calls_count" -le 1 ]; then
  109. clear_permission_state
  110. fi
  111. ;;
  112. *"kind: permission_prompt"*)
  113. # Emitted when a decision is submitted; don't notify here.
  114. clear_permission_state
  115. ask_user_pending=0
  116. ;;
  117. *"kind: assistant_turn_end"*)
  118. clear_permission_state
  119. ;;
  120. *"kind: session_idle"*)
  121. if [ "$busy" -eq 1 ]; then
  122. notify_task_finished "$win_addr"
  123. busy=0
  124. fi
  125. clear_permission_state
  126. ask_user_pending=0
  127. ;;
  128. esac
  129. done < <(tail -n0 -F "$log_file" 2>/dev/null)
  130. clear_permission_state
  131. trap - USR1
  132. }
  133. is_prompt_run=0
  134. for arg in "$@"; do
  135. case "$arg" in
  136. -p | --prompt | -i | --interactive)
  137. is_prompt_run=1
  138. break
  139. ;;
  140. esac
  141. done
  142. win_addr="$(hyprctl activewindow -j | jq -r '.address // empty')"
  143. if [ "$is_prompt_run" -eq 0 ] || [ "$#" -eq 0 ]; then
  144. started_epoch=$(date +%s)
  145. if [ -d "$HOME/.copilot/logs" ]; then
  146. start_interactive_monitor "$win_addr" "$started_epoch" &
  147. monitor_pid=$!
  148. trap 'kill "$monitor_pid" 2>/dev/null || true' EXIT
  149. fi
  150. # Debug logs include structured telemetry markers needed by the monitor.
  151. has_log_level=0
  152. for arg in "$@"; do
  153. case "$arg" in
  154. --log-level | --log-level=*)
  155. has_log_level=1
  156. break
  157. ;;
  158. esac
  159. done
  160. set +e
  161. if [ "$has_log_level" -eq 1 ]; then
  162. copilot "$@"
  163. else
  164. copilot --log-level debug "$@"
  165. fi
  166. exit_code=$?
  167. set -e
  168. kill "${monitor_pid:-}" 2>/dev/null || true
  169. exit "$exit_code"
  170. fi
  171. copilot --output-format json "$@" |
  172. tee >(
  173. jq -Rrc '
  174. (fromjson? | select(. != null)) as $e |
  175. if $e.type=="result" then {"kind":"done","code":($e.exitCode // 0)}
  176. elif $e.type=="tool.execution_start" and ($e.data.toolName=="ask_user") then {"kind":"action"}
  177. else empty end
  178. ' |
  179. while read -r ev; do
  180. kind="$(jq -r '.kind' <<<"$ev")"
  181. if [ "$kind" = "action" ]; then
  182. notify_action_required "$win_addr"
  183. else
  184. notify_task_finished "$win_addr"
  185. fi
  186. done
  187. ) |
  188. jq -Rr '(fromjson? | select(.type=="assistant.message") | (.data.content // empty))'