feat(insights): flag-gated Insights tab backed by a bundled pipe - #6235
Open
louis030195 wants to merge 2 commits into
Open
feat(insights): flag-gated Insights tab backed by a bundled pipe#6235louis030195 wants to merge 2 commits into
louis030195 wants to merge 2 commits into
Conversation
louis030195
force-pushed
the
claude/insights-tab-v1
branch
2 times, most recently
from
August 14, 2026 00:45
9ea9c64 to
9c16d14
Compare
Adds an Insights tab behind the `insights_tab` PostHog flag: a hardcoded
dashboard whose data is produced by a bundled scheduled pipe, rather than an
AI-generated Live View.
No new endpoint and no new aggregation. `/activity-summary` already returns
per-app minutes with idle gaps excluded, total active minutes, frame counts and
recording status. The pipe curls it and writes the response verbatim; the page
parses that file. Re-deriving any of it would just be a second definition of
"active time" to keep in sync.
pipe (hourly) -> GET /activity-summary -> insights.json
-> auto_register_pipe_artifacts -> GET /artifacts
-> readViewerFile -> hardcoded page
Why not Live View blocks: that system is capped at 7 hardcoded block kinds with
maxItems 20-60 and MAX_BLOCKS 24, and its values land in a namespace
deliberately excluded from GET /artifacts. All of it exists to support
user-authored AI-generated layouts, which is the opposite of a fixed dashboard.
v1 ships two sections: a capture receipt and time by category. Coverage % and
gap counts were cut during implementation because we cannot know when the
machine was on, so those numbers would have been fabricated. A capture streak
is not here either: it needs a durable daily series, which nothing currently
stores.
Notes:
- The pipe ships `enabled: false`. Opening the tab enables it, so a user who
never opens Insights never pays for an hourly run.
- pipe.md is a copy job: one curl and an explicit "byte for byte" instruction.
The model cannot influence a number.
- Categories reuse getAppCategory from lib/utils.ts (now exported) rather than
forking the mapping table.
- The comparison ladder's divisors are asserted strictly increasing, so the
headline count can never drop as recorded time grows.
- Flag fails closed on an unresolved flag, and a stale ?section=insights now
redirects when the flag is off - the existing effect only handled enterprise
hidden sections.
Tests: 1 Rust pipe-contract test, 17 Vitest (defensive parsing of partial and
non-finite payloads, category folding, ladder monotonicity, flag gate), 1 E2E
spec registered in the coverage manifest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The category chart was worthless on real data. Measured on a live 7-day window: browser 64%, other 33%, communication 3%, dev 0%. Claude — the second-biggest app — landed in "other", along with ChatGPT, WezTerm and Ghostty, because lib/utils.ts maps app *names* against a list written before those tools existed. And one browser was 64% of everything, which is the least informative fact available. An app-name lookup table cannot fix this. The work is inside the browser. So categories are now natural-language descriptors and the bundled pipe labels each *surface* — an app, or a domain within a browser — with exactly one key from a closed set. Approach borrowed from Dayflow (MIT, © 2025 Jerry Liu), which classifies against category descriptions rather than a static bundle-id table; the descriptors here are our own. The split that keeps this honest: every minute still comes from the engine's SQL, which already excludes idle gaps. The model only ever chooses a label. A bad run can mislabel time; it cannot invent it. Anything outside the closed set becomes visible `uncategorised` minutes rather than a plausible wrong bar, and a test asserts the totals hold under an empty, hallucinated, or mismatched label map. Also raises the window cap. /activity-summary hardcoded LIMIT 30, which attributed only 46% of browser minutes to a domain — the URLs were there (98% of Arc frames carry one), we were truncating them. Adds a `max_windows` param (default 30, clamped to 300) mirroring the existing `max_snippets`. Measured on the same window: domain-attributed time goes 535 -> 964 minutes, 46% -> 82% of browser time. Net effect on the same week: instead of "browser 64% / other 33%", the top surfaces are Claude 15h19m, Arc 10h20m, web.whatsapp.com 2h39m, publish.buffer.com 1h20m, app.slack.com 34m, docs.google.com 30m — 96% of active time in the top 16, each one labellable. lib/utils.ts is untouched again; getAppCategory stays private. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
louis030195
force-pushed
the
claude/insights-tab-v1
branch
from
August 14, 2026 15:39
9c16d14 to
e2afcd7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
An Insights tab behind the
insights_tabPostHog flag: a hardcoded dashboard whose data is produced by a bundled scheduled pipe, rather than an AI-generated Live View.HTML mockup at the exact v1 scope. Not a screenshot of the built app — see Validation.
How
No new endpoint and no new aggregation.
/activity-summaryalready returns per-app minutes with idle gaps excluded, total active minutes, frame counts and recording status. The pipe curls it and writes the response verbatim; the page parses that file. Re-deriving any of it would just be a second definition of "active time" to keep in sync withIDLE_CAP_SECS.pipe.mdis a copy job — onecurl, an explicit "byte for byte" instruction, nothing else. The model cannot influence a number.Not Live View blocks: that system is capped at 7 hardcoded block kinds with
maxItems20–60 andMAX_BLOCKS = 24(live_views.rs:205), and its values land in a namespace deliberately excluded fromGET /artifacts(outputs.rs:541). All of it exists to support user-authored AI-generated layouts, which is the opposite of a fixed dashboard.Scope
Two sections: capture receipt, time by category.
Cut during implementation:
appsandlast frameare verifiable and say enough.Reference notes
This came out of decompiling Wispr Flow 1.6.492. Two of their mechanics are worth having (the re-scaling comparison ladder, and eventually the streak heatmap). Three of their bugs are worth not repeating — the ladder one has a regression test here:
7500 → 9000 → 10000 → 4300, so at 49,999 words you have written "11 short film scripts" and at 50,001 you have written "1 book chapter". The count drops as you do more.describeActiveTimeasserts strictly increasing divisors.min(UTC date, local date), and their legacy streak SQL usesstrftime('%M-%d')—%Mis minutes, not month. Relevant if we add the streak later.Notes
enabled: false. Opening the tab enables it, so a user who never opens Insights never pays for an hourly run.getAppCategoryfromlib/utils.ts(now exported) rather than forking the mapping table.?section=insightsnow redirects when the flag is off — the existing effect only handled enterprise hidden sections, which was a real gap.hidden_sectionssupport is free: the policy filter runs before the flag.Validation
Run locally, all green:
cargo test -p screenpipe-core --lib pipes::— 254 passed, incl. a new test pinning the pipe's frontmatter contract (disabled, hourly, declaresinsights.json, prompt stays a copy job)bunx vitest run lib/insights lib/insights-rollout.test.ts— 17 passed (defensive parsing of partial/non-finite/error payloads, category folding, ladder monotonicity, flag gate)bun run typecheck— cleanbun run coverage:all:check— cleangit diff --check— cleanNot run locally, flagged honestly:
e2e/specs/insights-tab.spec.ts— written and registered, never executed here; CI is its first run.localStoragegap); identical count verified on a clean tree viagit stash.Rollout
Flag
insights_tabdoes not exist in PostHog yet — the tab is invisible to everyone until it is created and assigned. Suggested: create it disabled, enable for internal accounts, confirm the hourly pipe writesinsights.jsonand the numbers agree with Timeline, then ramp.🤖 Generated with Claude Code
Update: the category chart was broken, and Dayflow's approach fixes it
I measured the first version against a real 7-day window and it was worthless:
Claude — the second-biggest app — landed in other, with ChatGPT, WezTerm and Ghostty.
lib/utils.tsmatches app names against a list written before those tools existed, and one browser was 64% of everything. An app-name lookup table cannot fix that, because the work is inside the browser.What I took from Dayflow
Dayflow (MIT, © 2025 Jerry Liu) doesn't keep an app table. Categories are natural-language descriptors and a model classifies against them, with
"Return the category exactly as written. Allowed values: [...]"and a normaliser that rejects anything off-list. That's why it can tell "researching on YouTube" from "watching cat videos".Ported the approach, wrote our own descriptors rather than copying their strings into a commercial-licensed repo.
What keeps it honest
The pipe labels surfaces — an app, or a domain within a browser — with one key from a closed set. Minutes still come entirely from the engine's SQL, which already excludes idle gaps. The model chooses labels; it never produces a number. A bad run can mislabel time, it cannot invent it. Off-list keys become visible
uncategorisedminutes, and a test asserts totals hold under an empty, hallucinated, or mismatched label map.Also: we were throwing away the URLs
/activity-summaryhardcodedLIMIT 30on windows, so only 46% of browser minutes could be attributed to a domain. The URLs were there all along — 98% of Arc frames carry one. Addedmax_windows(default 30, clamped 300), mirroring the existingmax_snippets. Measured on the same window: 535 → 964 domain-attributed minutes, 46% → 82%.Result on the same week
96% of active time in the top 16 surfaces, every one of them labellable — instead of "browser 64% / other 33%".
lib/utils.tsis untouched again;getAppCategorystays private.Still not verified: the model labelling itself. The pipe has never run end to end here — I validated the surface split and the coverage numbers against the live local API and the database, but no model has assigned a label yet. That is the first thing to check when the flag goes on.
Sources: Dayflow · LICENSE