You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Users collect a lot of activity data through ActivityWatch, but interpreting raw time breakdowns can be tedious. A one-click AI summary would help users quickly understand their habits without manually analyzing charts.
Proposed Solution
Add a new AI Summary page under the Tools menu that:
Queries the past week's app usage data via the existing Query API (aw-watcher-window bucket)
Sends a formatted summary to an AI endpoint (user-configured provider)
Displays a concise, friendly Chinese/English summary
Key Design Choices
Provider selection: OpenAI / DeepSeek / Ollama (local) -- user picks which one
API key stored in localStorage -- never touches the server, privacy-preserving
Date range selector: last 7 days / 30 days / this week / this month
Copy & download buttons for saving the generated summary
All data stays local -- only the formatted text (app names + durations) is sent to the AI
This is a great fit for AW and the design already gets the privacy tradeoffs right (client-side aggregation, key in localStorage, opt-in send).
One reference that might save you some design time: gptme-activity-summary is a Python CLI that does the same AW-query → LLM-summary pipeline for human work reports:
gptme-activity-summary daily --mode human --date yesterday --raw # data only, no LLM call
gptme-activity-summary weekly --mode human --week last # LLM narrative summary
The --raw mode is the useful bit for your Vue prototype — it shows the intermediate data shape we settled on after a few iterations: top apps (name + duration + distinct-title count), top browser domains, and AW-category rollups (using the user's existing categorization rules, not just app names). Splitting "aggregate query results into that compact shape" from "hand the formatted text to whichever LLM provider" as two separate steps made provider-swapping trivial later — worth keeping that same seam in AiSummary.vue even though your stack is JS, not Python.
Happy to share more detail on the aggregation logic if useful. Looking forward to seeing this land — go ahead and start prototyping.
Thanks for picking up the seam split — that's the move that made provider-swapping easy later.
Here's the compact aggregation shape we ended up with in gptme-activity-summary (Python, but the TS interfaces map directly). Putting them in AiSummary.vue is probably the cleanest spot:
When the user has no rules configured, AW returns every event as ["Uncategorized"] — gate the category section in your output on categories.some(c => c.category[0] !== "Uncategorized") so they don't get a meaningless "Uncategorized: 100%" block.
The intermediate-format function lives at gptme-contrib/packages/gptme-activity-summary/src/gptme_activity_summary/aw_data.py (format_aw_activity_for_prompt + the _fetch_* helpers). The one detail that surprised us: use category_total as the denominator for category percentages, not total_active_seconds — the categorize() query and the apps query are separate HTTP round-trips, so their totals don't always match exactly.
For the LLM side: keep the prompt a single markdown string of SummaryInput, and let the provider adapters just translate that into a chat-completions request. OpenAI ↔ DeepSeek ↔ Ollama becomes a config-only change.
Happy to answer more specific questions on the aggregation logic as you build. Go for it.
Just checking in — any progress on the prototype? Happy to answer questions or look at a draft PR if you want early feedback. If life got busy and you'd prefer Bob just build the minimal version, let me know and I can take a crack at it.
Activity
TimeToBuildBob commentedon Jul 1, 2026
This is a great fit for AW and the design already gets the privacy tradeoffs right (client-side aggregation, key in localStorage, opt-in send).
One reference that might save you some design time: gptme-activity-summary is a Python CLI that does the same AW-query → LLM-summary pipeline for human work reports:
The
--rawmode is the useful bit for your Vue prototype — it shows the intermediate data shape we settled on after a few iterations: top apps (name + duration + distinct-title count), top browser domains, and AW-category rollups (using the user's existing categorization rules, not just app names). Splitting "aggregate query results into that compact shape" from "hand the formatted text to whichever LLM provider" as two separate steps made provider-swapping trivial later — worth keeping that same seam inAiSummary.vueeven though your stack is JS, not Python.Happy to share more detail on the aggregation logic if useful. Looking forward to seeing this land — go ahead and start prototyping.
7nicetoseeu commentedon Jul 1, 2026
Thanks, this is very helpful.
I took your suggestion and plan to keep a clear seam between:
For the first pass, I’m aiming for a minimal but usable Tools -> AI Summary page in �w-webui that:
I’ll likely keep the first PR intentionally small and leave things like richer aggregation fields and streaming as follow-ups.
I’m starting from that structure now and will open a PR once the prototype is ready.
TimeToBuildBob commentedon Jul 1, 2026
Thanks for picking up the seam split — that's the move that made provider-swapping easy later.
Here's the compact aggregation shape we ended up with in
gptme-activity-summary(Python, but the TS interfaces map directly). Putting them inAiSummary.vueis probably the cleanest spot:The two AQL queries worth reusing — both already supported by your existing
queries.tsinfrastructure (bid_window,bid_afk,filter_afk):1. Top apps (with AFK filtering when the AFK bucket exists):
Then walk the result and count
data.titleperdata.appfortitleCount.2. Category rollup (uses the user's existing AW categorization rules via the settings API, then the AQL
categorize()fn):When the user has no rules configured, AW returns every event as
["Uncategorized"]— gate the category section in your output oncategories.some(c => c.category[0] !== "Uncategorized")so they don't get a meaningless "Uncategorized: 100%" block.The intermediate-format function lives at
gptme-contrib/packages/gptme-activity-summary/src/gptme_activity_summary/aw_data.py(format_aw_activity_for_prompt+ the_fetch_*helpers). The one detail that surprised us: usecategory_totalas the denominator for category percentages, nottotal_active_seconds— the categorize() query and the apps query are separate HTTP round-trips, so their totals don't always match exactly.For the LLM side: keep the prompt a single markdown string of
SummaryInput, and let the provider adapters just translate that into a chat-completions request. OpenAI ↔ DeepSeek ↔ Ollama becomes a config-only change.Happy to answer more specific questions on the aggregation logic as you build. Go for it.
TimeToBuildBob commentedon Jul 15, 2026
Just checking in — any progress on the prototype? Happy to answer questions or look at a draft PR if you want early feedback. If life got busy and you'd prefer Bob just build the minimal version, let me know and I can take a crack at it.
feat(ai-summary): add client-side AI activity summary page
feat(ai-summary): add client-side AI activity summary page (ActivityW…
feat(ai-summary): add client-side AI activity summary page (#922)