copilot-notify 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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 log_file
  26. local line
  27. local busy=0
  28. local ask_user_pending=0
  29. # Wait for the process log created by this copilot invocation.
  30. for _ in $(seq 1 60); do
  31. log_file=$(find "$HOME/.copilot/logs" -maxdepth 1 -name 'process-*.log' -newermt "@$started_epoch" -print 2>/dev/null | sort | tail -n1 || true)
  32. if [ -n "${log_file:-}" ]; then
  33. break
  34. fi
  35. sleep 0.1
  36. done
  37. [ -n "${log_file:-}" ] || return 0
  38. tail -n0 -F "$log_file" 2>/dev/null | while IFS= read -r line; do
  39. if [ "$ask_user_pending" -gt 0 ]; then
  40. ask_user_pending=$((ask_user_pending - 1))
  41. fi
  42. case "$line" in
  43. *"kind: assistant_turn_start"*)
  44. busy=1
  45. ;;
  46. *'"name": "ask_user"'*)
  47. # "ask_user" appears in tool schema on every turn; only alert if a
  48. # real tool call follows immediately with a question payload.
  49. ask_user_pending=8
  50. ;;
  51. *'"arguments": "{\"question\":'*)
  52. if [ "$ask_user_pending" -gt 0 ]; then
  53. notify_action_required "$win_addr"
  54. ask_user_pending=0
  55. fi
  56. ;;
  57. *"kind: permission_prompt"*)
  58. notify_action_required "$win_addr"
  59. ask_user_pending=0
  60. ;;
  61. *"kind: session_idle"*)
  62. if [ "$busy" -eq 1 ]; then
  63. notify_task_finished "$win_addr"
  64. busy=0
  65. fi
  66. ask_user_pending=0
  67. ;;
  68. esac
  69. done
  70. }
  71. is_prompt_run=0
  72. for arg in "$@"; do
  73. case "$arg" in
  74. -p | --prompt | -i | --interactive)
  75. is_prompt_run=1
  76. break
  77. ;;
  78. esac
  79. done
  80. win_addr="$(hyprctl activewindow -j | jq -r '.address // empty')"
  81. if [ "$is_prompt_run" -eq 0 ] || [ "$#" -eq 0 ]; then
  82. started_epoch=$(date +%s)
  83. if [ -d "$HOME/.copilot/logs" ]; then
  84. start_interactive_monitor "$win_addr" "$started_epoch" &
  85. monitor_pid=$!
  86. trap 'kill "$monitor_pid" 2>/dev/null || true' EXIT
  87. fi
  88. # Debug logs include structured telemetry markers needed by the monitor.
  89. has_log_level=0
  90. for arg in "$@"; do
  91. case "$arg" in
  92. --log-level | --log-level=*)
  93. has_log_level=1
  94. break
  95. ;;
  96. esac
  97. done
  98. set +e
  99. if [ "$has_log_level" -eq 1 ]; then
  100. copilot "$@"
  101. else
  102. copilot --log-level debug "$@"
  103. fi
  104. exit_code=$?
  105. set -e
  106. kill "${monitor_pid:-}" 2>/dev/null || true
  107. exit "$exit_code"
  108. fi
  109. copilot --output-format json "$@" |
  110. tee >(
  111. jq -Rrc '
  112. (fromjson? | select(. != null)) as $e |
  113. if $e.type=="result" then {"kind":"done","code":($e.exitCode // 0)}
  114. elif $e.type=="tool.execution_start" and ($e.data.toolName=="ask_user") then {"kind":"action"}
  115. else empty end
  116. ' |
  117. while read -r ev; do
  118. kind="$(jq -r '.kind' <<<"$ev")"
  119. if [ "$kind" = "action" ]; then
  120. notify_action_required "$win_addr"
  121. else
  122. notify_task_finished "$win_addr"
  123. fi
  124. done
  125. ) |
  126. jq -Rr '(fromjson? | select(.type=="assistant.message") | (.data.content // empty))'