From 57a969e6a7388ef08348868283d2dc7bd164acf0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 17:43:02 +0000 Subject: [PATCH 1/3] feat(heartbeat): skip duplicate activity events Add duplicate detection to avoid sending redundant heartbeats when the file path and cursor position haven't changed. This reduces unnecessary socket traffic when the user hasn't made any actual changes. - Track last activity state (file path, line number, cursor position) - Skip events where all three values match the previous event - Write events are never skipped (always sent to socket) - clear_cache() now resets duplicate detection state --- lua/shelltime/heartbeat.lua | 57 +++++++++++++++++++++++++++++++++++++ tests/heartbeat_spec.lua | 7 +++++ 2 files changed, 64 insertions(+) diff --git a/lua/shelltime/heartbeat.lua b/lua/shelltime/heartbeat.lua index 5eaf6ef..48332a6 100644 --- a/lua/shelltime/heartbeat.lua +++ b/lua/shelltime/heartbeat.lua @@ -16,6 +16,13 @@ local pending_heartbeats = {} -- Last heartbeat time per file (for debouncing) local last_heartbeat_time = {} +-- Last activity state (for duplicate detection) +local last_activity = { + file_path = nil, + line_number = nil, + cursor_position = nil, +} + -- Autocmd group local augroup = nil @@ -82,6 +89,38 @@ local function should_send_heartbeat(file_path, is_write) return false end +--- Check if activity is a duplicate (same file and cursor position) +---@param file_path string File path +---@param line_number number Line number (1-indexed) +---@param cursor_position number Cursor column (0-indexed) +---@param is_write boolean Whether this is a write event +---@return boolean True if duplicate (should skip) +local function is_duplicate_activity(file_path, line_number, cursor_position, is_write) + -- Write events are never considered duplicates + if is_write then + return false + end + + -- Check if same as last activity + if last_activity.file_path == file_path + and last_activity.line_number == line_number + and last_activity.cursor_position == cursor_position then + return true + end + + return false +end + +--- Update last activity state +---@param file_path string File path +---@param line_number number Line number (1-indexed) +---@param cursor_position number Cursor column (0-indexed) +local function update_last_activity(file_path, line_number, cursor_position) + last_activity.file_path = file_path + last_activity.line_number = line_number + last_activity.cursor_position = cursor_position +end + --- Create heartbeat data for current buffer ---@param bufnr number Buffer number ---@param is_write boolean Whether this is a write event @@ -147,10 +186,23 @@ local function on_event(is_write) local file_path = vim.api.nvim_buf_get_name(bufnr) + -- Get cursor position for duplicate detection + local cursor = vim.api.nvim_win_get_cursor(0) + local line_number = cursor[1] + local cursor_position = cursor[2] + + -- Skip duplicate events (same file and cursor position) + if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then + return + end + if not should_send_heartbeat(file_path, is_write) then return end + -- Update last activity state + update_last_activity(file_path, line_number, cursor_position) + local heartbeat = create_heartbeat(bufnr, is_write) if heartbeat then add_heartbeat(heartbeat) @@ -223,6 +275,11 @@ end --- Clear debounce cache function M.clear_cache() last_heartbeat_time = {} + last_activity = { + file_path = nil, + line_number = nil, + cursor_position = nil, + } end return M diff --git a/tests/heartbeat_spec.lua b/tests/heartbeat_spec.lua index 0286add..9f43a7c 100644 --- a/tests/heartbeat_spec.lua +++ b/tests/heartbeat_spec.lua @@ -142,6 +142,13 @@ describe('shelltime.heartbeat', function() heartbeat.clear_cache() end) end) + + it('should reset last activity state for duplicate detection', function() + -- clear_cache should reset both debounce and duplicate tracking + heartbeat.clear_cache() + -- After clearing, the next event should not be considered duplicate + assert.equals(0, heartbeat.get_pending_count()) + end) end) describe('module exports', function() From fb4bc6bba5adfcc2dc0382fd9a3c9597aa591d13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 17:50:15 +0000 Subject: [PATCH 2/3] fix(heartbeat): correct order of debounce and duplicate checks Move debounce check before duplicate detection to ensure consistent behavior. Now duplicate detection only applies to events that pass debouncing, and last_activity tracks the position of the last sent heartbeat rather than any event. --- lua/shelltime/heartbeat.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/shelltime/heartbeat.lua b/lua/shelltime/heartbeat.lua index 48332a6..d5f73e3 100644 --- a/lua/shelltime/heartbeat.lua +++ b/lua/shelltime/heartbeat.lua @@ -191,12 +191,12 @@ local function on_event(is_write) local line_number = cursor[1] local cursor_position = cursor[2] - -- Skip duplicate events (same file and cursor position) - if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then + if not should_send_heartbeat(file_path, is_write) then return end - if not should_send_heartbeat(file_path, is_write) then + -- Skip duplicate events (same file and cursor position) + if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then return end From 4ee378798897714d7cf59d24a28592ba028c289e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 03:53:09 +0000 Subject: [PATCH 3/3] fix(heartbeat): update last_activity before debounce check Move update_last_activity() before the debounce check to fix race condition. Previously, if an event passed duplicate check but failed debounce, last_activity wasn't updated. This caused stale comparisons when debounce later passed. Now last_activity tracks the most recent unique position, regardless of whether a heartbeat was actually sent. --- lua/shelltime/heartbeat.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lua/shelltime/heartbeat.lua b/lua/shelltime/heartbeat.lua index d5f73e3..d6fffd5 100644 --- a/lua/shelltime/heartbeat.lua +++ b/lua/shelltime/heartbeat.lua @@ -191,18 +191,19 @@ local function on_event(is_write) local line_number = cursor[1] local cursor_position = cursor[2] - if not should_send_heartbeat(file_path, is_write) then - return - end - -- Skip duplicate events (same file and cursor position) if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then return end - -- Update last activity state + -- Update last activity state immediately after duplicate check + -- This ensures we track the latest position even if debounce blocks sending update_last_activity(file_path, line_number, cursor_position) + if not should_send_heartbeat(file_path, is_write) then + return + end + local heartbeat = create_heartbeat(bufnr, is_write) if heartbeat then add_heartbeat(heartbeat)