copilot-notify 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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: session_idle"*)
  58. if [ "$busy" -eq 1 ]; then
  59. notify_task_finished "$win_addr"
  60. busy=0
  61. fi
  62. ask_user_pending=0
  63. ;;
  64. esac
  65. done
  66. }
  67. is_prompt_run=0
  68. for arg in "$@"; do
  69. case "$arg" in
  70. -p | --prompt | -i | --interactive)
  71. is_prompt_run=1
  72. break
  73. ;;
  74. esac
  75. done
  76. win_addr="$(hyprctl activewindow -j | jq -r '.address // empty')"
  77. if [ "$is_prompt_run" -eq 0 ] || [ "$#" -eq 0 ]; then
  78. started_epoch=$(date +%s)
  79. if [ -d "$HOME/.copilot/logs" ]; then
  80. start_interactive_monitor "$win_addr" "$started_epoch" &
  81. monitor_pid=$!
  82. trap 'kill "$monitor_pid" 2>/dev/null || true' EXIT
  83. fi
  84. # Debug logs include structured telemetry markers needed by the monitor.
  85. has_log_level=0
  86. for arg in "$@"; do
  87. case "$arg" in
  88. --log-level | --log-level=*)
  89. has_log_level=1
  90. break
  91. ;;
  92. esac
  93. done
  94. set +e
  95. if [ "$has_log_level" -eq 1 ]; then
  96. copilot "$@"
  97. else
  98. copilot --log-level debug "$@"
  99. fi
  100. exit_code=$?
  101. set -e
  102. kill "${monitor_pid:-}" 2>/dev/null || true
  103. exit "$exit_code"
  104. fi
  105. copilot --output-format json "$@" |
  106. tee >(
  107. jq -Rrc '
  108. (fromjson? | select(. != null)) as $e |
  109. if $e.type=="result" then {"kind":"done","code":($e.exitCode // 0)}
  110. elif $e.type=="tool.execution_start" and ($e.data.toolName=="ask_user") then {"kind":"action"}
  111. else empty end
  112. ' |
  113. while read -r ev; do
  114. kind="$(jq -r '.kind' <<<"$ev")"
  115. if [ "$kind" = "action" ]; then
  116. notify_action_required "$win_addr"
  117. else
  118. notify_task_finished "$win_addr"
  119. fi
  120. done
  121. ) |
  122. jq -Rr '(fromjson? | select(.type=="assistant.message") | (.data.content // empty))'