From 66ff75b649e8f727c7f8bd5ce2b71bafe3fbc8ee Mon Sep 17 00:00:00 2001 From: mldangelo Date: Sun, 25 Jan 2026 16:10:44 -0800 Subject: [PATCH 1/2] feat(slack): add real-time message polling to chat Implement message polling in slack_chat() so incoming messages are displayed as they arrive. Previously, the polling block was empty. - Add slack_get_latest_ts() to get newest message timestamp - Add slack_poll_new_messages() to fetch messages since a timestamp - Track last_ts after displaying initial messages - Poll every 5 seconds and display new messages in chronological order - Update last_ts after sending to avoid showing own messages twice Co-Authored-By: Claude Opus 4.5 --- src/crabcode | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/crabcode b/src/crabcode index cf16af4..0445a92 100755 --- a/src/crabcode +++ b/src/crabcode @@ -2573,6 +2573,36 @@ slack_display_messages() { done <<< "$messages" } +# Get timestamp of latest message in a channel (for polling baseline) +slack_get_latest_ts() { + local channel_id="$1" + local token=$(slack_get_token) + + curl -s -H "Authorization: Bearer $token" \ + "https://slack.com/api/conversations.history?channel=$channel_id&limit=1" \ + | jq -r '.messages[0].ts // "0"' +} + +# Poll for new messages since a given timestamp +slack_poll_new_messages() { + local channel_id="$1" + local since_ts="$2" + local token=$(slack_get_token) + + # Get messages newer than since_ts + local response=$(curl -s -H "Authorization: Bearer $token" \ + "https://slack.com/api/conversations.history?channel=$channel_id&oldest=$since_ts&limit=20") + + local ok=$(echo "$response" | jq -r '.ok') + if [ "$ok" != "true" ]; then + return 1 + fi + + # Return messages (excluding the one at exactly since_ts), in chronological order + echo "$response" | jq -c --arg ts "$since_ts" \ + '.messages | map(select(.ts != $ts)) | reverse | .[]' +} + # Interactive chat mode slack_chat() { local target="$1" @@ -2624,6 +2654,9 @@ slack_chat() { echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + # Get latest message timestamp for polling baseline + local last_ts=$(slack_get_latest_ts "$channel_id") + # Chat loop local last_check=$(date +%s) @@ -2635,6 +2668,8 @@ slack_chat() { if [ -n "$message" ]; then if slack_post_message "$channel_id" "$message"; then echo -e "${GRAY}[sent]${NC}" + # Update last_ts to avoid re-displaying our own message + last_ts=$(slack_get_latest_ts "$channel_id") fi fi fi @@ -2642,7 +2677,36 @@ slack_chat() { # Poll for new messages every 5 seconds local now=$(date +%s) if [ $((now - last_check)) -ge 5 ]; then - # Check for new messages (would need to track last seen timestamp for proper implementation) + # Clear the prompt line before showing new messages + echo -ne "\r\033[K" + + # Poll for new messages + local new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts") + + if [ -n "$new_messages" ]; then + while IFS= read -r msg; do + local ts=$(echo "$msg" | jq -r '.ts') + local time=$(echo "$msg" | jq -r '.ts | tonumber | strftime("%H:%M")') + local user_id=$(echo "$msg" | jq -r '.user // empty') + local text=$(echo "$msg" | jq -r '.text') + local bot_name=$(echo "$msg" | jq -r '.bot_profile.name // empty') + + local sender + if [ -n "$bot_name" ]; then + sender="$bot_name" + elif [ -n "$user_id" ]; then + sender=$(slack_get_username_cached "$user_id") + else + sender="unknown" + fi + + echo -e "${GRAY}[$time]${NC} ${CYAN}$sender${NC}: $text" + + # Update last seen timestamp + last_ts="$ts" + done <<< "$new_messages" + fi + last_check=$now fi done From 885182d68a50e82c4d07c68949064641ba9a6ad4 Mon Sep 17 00:00:00 2001 From: mldangelo Date: Mon, 26 Jan 2026 00:12:34 -0800 Subject: [PATCH 2/2] fix(slack): address PR review feedback for chat polling - Add Slack API error validation to slack_get_latest_ts - Handle polling errors gracefully to prevent chat loop crashes with set -e - Remove post-send last_ts update to avoid skipping messages - Increase poll limit from 20 to 100 for busy channels - Sanitize message text to prevent terminal escape sequence injection - Removes CSI sequences (colors, cursor, screen control) - Removes OSC sequences (title manipulation) - Strips control characters (bell, backspace, etc.) Co-Authored-By: Claude Opus 4.5 --- src/crabcode | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/crabcode b/src/crabcode index 0445a92..ffbad29 100755 --- a/src/crabcode +++ b/src/crabcode @@ -2578,9 +2578,13 @@ slack_get_latest_ts() { local channel_id="$1" local token=$(slack_get_token) - curl -s -H "Authorization: Bearer $token" \ - "https://slack.com/api/conversations.history?channel=$channel_id&limit=1" \ - | jq -r '.messages[0].ts // "0"' + local response=$(curl -s -H "Authorization: Bearer $token" \ + "https://slack.com/api/conversations.history?channel=$channel_id&limit=1") + local ok=$(echo "$response" | jq -r '.ok') + if [ "$ok" != "true" ]; then + return 1 + fi + echo "$response" | jq -r '.messages[0].ts // "0"' } # Poll for new messages since a given timestamp @@ -2589,9 +2593,9 @@ slack_poll_new_messages() { local since_ts="$2" local token=$(slack_get_token) - # Get messages newer than since_ts + # Get messages newer than since_ts (limit=100 to handle busy channels) local response=$(curl -s -H "Authorization: Bearer $token" \ - "https://slack.com/api/conversations.history?channel=$channel_id&oldest=$since_ts&limit=20") + "https://slack.com/api/conversations.history?channel=$channel_id&oldest=$since_ts&limit=100") local ok=$(echo "$response" | jq -r '.ok') if [ "$ok" != "true" ]; then @@ -2655,7 +2659,10 @@ slack_chat() { echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # Get latest message timestamp for polling baseline - local last_ts=$(slack_get_latest_ts "$channel_id") + local last_ts + if ! last_ts=$(slack_get_latest_ts "$channel_id"); then + last_ts="0" + fi # Chat loop local last_check=$(date +%s) @@ -2668,8 +2675,8 @@ slack_chat() { if [ -n "$message" ]; then if slack_post_message "$channel_id" "$message"; then echo -e "${GRAY}[sent]${NC}" - # Update last_ts to avoid re-displaying our own message - last_ts=$(slack_get_latest_ts "$channel_id") + # Note: last_ts is updated when polling new messages to avoid skipping any + # messages that arrive between send and the next poll fi fi fi @@ -2680,15 +2687,22 @@ slack_chat() { # Clear the prompt line before showing new messages echo -ne "\r\033[K" - # Poll for new messages - local new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts") + # Poll for new messages (handle errors gracefully to avoid crashing chat loop) + local new_messages="" + if ! new_messages=$(slack_poll_new_messages "$channel_id" "$last_ts"); then + # Transient API error (e.g., rate limiting) - skip this poll cycle + last_check=$now + continue + fi if [ -n "$new_messages" ]; then while IFS= read -r msg; do local ts=$(echo "$msg" | jq -r '.ts') local time=$(echo "$msg" | jq -r '.ts | tonumber | strftime("%H:%M")') local user_id=$(echo "$msg" | jq -r '.user // empty') - local text=$(echo "$msg" | jq -r '.text') + # Sanitize text to prevent terminal escape sequence injection + # Remove CSI sequences (\033[...X), OSC sequences (\033]...\007), then control chars + local text=$(echo "$msg" | jq -r '.text' | sed $'s/\033\\[[0-9;]*[a-zA-Z]//g; s/\033\\][^\007]*\007//g' | tr -d '\000-\010\013\014\016-\037') local bot_name=$(echo "$msg" | jq -r '.bot_profile.name // empty') local sender