From 636df2c00786bcb07419adbef7fcb05d81b6b11c Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 4 Mar 2026 15:55:09 +0000 Subject: [PATCH 1/2] feat(timeline): add 'Merge by app' filter to reduce event flooding Apps like Adobe Illustrator generate hundreds of tiny events when toggling UI panels (e.g. pressing TAB), flooding the Timeline view. This adds a "Merge by app" toggle in the Filters panel that merges adjacent events from the same application within a 30-second gap window into single blocks. Only affects `currentwindow` bucket events. Applied before AFK filtering so the merge and AFK filters compose correctly. Fixes: ActivityWatch/activitywatch#1165 --- src/views/Timeline.vue | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/views/Timeline.vue b/src/views/Timeline.vue index 02d1dd75..9bfa0984 100644 --- a/src/views/Timeline.vue +++ b/src/views/Timeline.vue @@ -38,6 +38,12 @@ div td b-form-checkbox(v-model="filter_afk" size="sm" switch) | Filter AFK + tr + th.pt-2.pr-3 + label Merge: + td + b-form-checkbox(v-model="filter_merge_similar" size="sm" switch) + | Merge by app tr th.pt-2.pr-3 label Categories: @@ -110,6 +116,7 @@ export default { filter_client: null, filter_duration: null, filter_afk: false, + filter_merge_similar: false, filter_categories: [], swimlane: null, updateTimelineWindow: true, @@ -143,6 +150,9 @@ export default { if (this.filter_afk) { desc.push('AFK filtered'); } + if (this.filter_merge_similar) { + desc.push('merged by app'); + } if (this.filter_categories.length > 0) { desc.push( this.filter_categories.length + @@ -178,6 +188,10 @@ export default { this.updateTimelineWindow = false; this.getBuckets(); }, + filter_merge_similar() { + this.updateTimelineWindow = false; + this.getBuckets(); + }, filter_categories() { this.updateTimelineWindow = false; this.getBuckets(); @@ -251,6 +265,13 @@ export default { } } + // Merge adjacent events by app name for window buckets. + // Reduces visual clutter when apps produce many small events (e.g. title + // changes from toggling UI panels). See: activitywatch#1165 + if (this.filter_merge_similar) { + buckets = this._applyMergeSimilar(buckets); + } + // AFK filtering: use query engine to filter window events by AFK status if (this.filter_afk) { buckets = await this._applyAfkFilter(buckets); @@ -259,6 +280,44 @@ export default { this.buckets = buckets; }, + // Merges adjacent events with the same app name within window buckets. + // This collapses rapid title changes (e.g. toggling UI panels) into single + // blocks per app, fixing timeline flooding for apps like Adobe Illustrator. + _applyMergeSimilar: function (buckets) { + return buckets.map(bucket => { + if (bucket.type !== 'currentwindow' || !bucket.events || bucket.events.length <= 1) { + return bucket; + } + + const sorted = [...bucket.events].sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + const merged = []; + let current = { ...sorted[0] }; + + for (let i = 1; i < sorted.length; i++) { + const next = sorted[i]; + const currentEnd = new Date(current.timestamp).getTime() + current.duration * 1000; + const nextStart = new Date(next.timestamp).getTime(); + const gap = nextStart - currentEnd; + + // Merge if same app and gap is small (< 30 seconds) + if (current.data?.app && current.data.app === next.data?.app && gap < 30000) { + const nextEnd = nextStart + next.duration * 1000; + current.duration = + (Math.max(currentEnd, nextEnd) - new Date(current.timestamp).getTime()) / 1000; + } else { + merged.push(current); + current = { ...next }; + } + } + merged.push(current); + + return { ...bucket, events: merged }; + }); + }, + // Replaces raw window bucket events with AFK-filtered events via aw query engine. // Also hides AFK status buckets since they're used for filtering, not display. _applyAfkFilter: async function (buckets) { From 0dff89de58d0d363b0c167c1e1a20c99131e64fe Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 23 Mar 2026 13:53:58 +0000 Subject: [PATCH 2/2] fix(timeline): apply AFK filter before merge-similar to prevent discard When both filters are active, _applyAfkFilter replaces currentwindow bucket events with fresh results from the backend query engine, which silently discarded any merges performed by _applyMergeSimilar. Reversing the order ensures merge-similar operates on already-filtered events, so both filters compose correctly. Fixes review feedback from Greptile. --- src/views/Timeline.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/views/Timeline.vue b/src/views/Timeline.vue index 9bfa0984..3951605c 100644 --- a/src/views/Timeline.vue +++ b/src/views/Timeline.vue @@ -265,18 +265,19 @@ export default { } } - // Merge adjacent events by app name for window buckets. - // Reduces visual clutter when apps produce many small events (e.g. title - // changes from toggling UI panels). See: activitywatch#1165 - if (this.filter_merge_similar) { - buckets = this._applyMergeSimilar(buckets); - } - // AFK filtering: use query engine to filter window events by AFK status if (this.filter_afk) { buckets = await this._applyAfkFilter(buckets); } + // Merge adjacent events by app name for window buckets. + // Runs after AFK filtering so merges operate on already-filtered events. + // Reduces visual clutter from apps that produce many small events (e.g. + // Adobe Illustrator's TAB key toggling UI panels). See: activitywatch#1165 + if (this.filter_merge_similar) { + buckets = this._applyMergeSimilar(buckets); + } + this.buckets = buckets; },