Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/views/Timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 +
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -256,9 +270,55 @@ export default {
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;
},

// 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) {
Expand Down
Loading