Skip to content

feat(insights): flag-gated Insights tab backed by a bundled pipe - #6235

Open
louis030195 wants to merge 2 commits into
mainfrom
claude/insights-tab-v1
Open

feat(insights): flag-gated Insights tab backed by a bundled pipe#6235
louis030195 wants to merge 2 commits into
mainfrom
claude/insights-tab-v1

Conversation

@louis030195

@louis030195 louis030195 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

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.

insights tab v1

HTML mockup at the exact v1 scope. Not a screenshot of the built app — see Validation.

How

pipe (hourly) → GET /activity-summary → insights.json
              → auto_register_pipe_artifacts → GET /artifacts
              → readViewerFile → hardcoded page

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 with IDLE_CAP_SECS.

pipe.md is a copy job — one curl, 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 maxItems 20–60 and MAX_BLOCKS = 24 (live_views.rs:205), and its values land in a namespace deliberately excluded from GET /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:

  • Coverage % and gap counts — we cannot know when the machine was on, so both would have been fabricated numbers on the panel whose whole job is honesty. apps and last frame are verifiable and say enough.
  • Capture streak — needs a durable daily series, which nothing currently stores. Adding one meant a new endpoint; not worth it for a heatmap.

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:

  • Their comparison ladder is non-monotonic: divisors run 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. describeActiveTime asserts strictly increasing divisors.
  • Their day boundary is min(UTC date, local date), and their legacy streak SQL uses strftime('%M-%d')%M is minutes, not month. Relevant if we add the streak later.
  • Their heatmap legend labels the lightest swatch "Less" while the empty state is a separate unlabelled colour.

Notes

  • The pipe ships enabled: false. Opening the tab enables it, so a user who never opens Insights never pays for an hourly run.
  • Categories reuse getAppCategory from lib/utils.ts (now exported) rather than forking the mapping table.
  • The flag fails closed on an unresolved flag. A stale ?section=insights now redirects when the flag is off — the existing effect only handled enterprise hidden sections, which was a real gap.
  • Enterprise hidden_sections support 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, declares insights.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 — clean
  • bun run coverage:all:check — clean
  • git diff --check — clean

Not run locally, flagged honestly:

  • e2e/specs/insights-tab.spec.ts — written and registered, never executed here; CI is its first run.
  • No screenshot of the built app. The image above is an HTML mockup, not compiled UI.
  • Full Vitest suite reports 66 pre-existing failures (jsdom localStorage gap); identical count verified on a clean tree via git stash.

Rollout

Flag insights_tab does 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 writes insights.json and 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:

browser         64%      other  33%      communication 3%
media            0%      productivity 0%      dev  0%   ← one frame

Claude — the second-biggest app — landed in other, with ChatGPT, WezTerm and Ghostty. lib/utils.ts matches 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 uncategorised minutes, and a test asserts totals hold under an empty, hallucinated, or mismatched label map.

Also: we were throwing away the URLs

/activity-summary hardcoded LIMIT 30 on 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. Added max_windows (default 30, clamped 300), mirroring the existing max_snippets. Measured on the same window: 535 → 964 domain-attributed minutes, 46% → 82%.

Result on the same week

15h 19m  app:Claude              1h 19m  app:Notion Calendar
10h 20m  app:Arc                    43m  web:app.loops.so
 2h 39m  web:web.whatsapp.com       34m  web:app.slack.com
 1h 54m  app:ChatGPT                30m  web:docs.google.com
 1h 20m  web:publish.buffer.com     22m  web:x.com

96% of active time in the top 16 surfaces, every one of them labellable — instead of "browser 64% / other 33%".

lib/utils.ts is untouched again; getAppCategory stays 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

@louis030195
louis030195 force-pushed the claude/insights-tab-v1 branch 2 times, most recently from 9ea9c64 to 9c16d14 Compare August 14, 2026 00:45
@louis030195 louis030195 changed the title feat(insights): flag-gated Insights tab backed by a deterministic pipe rollup feat(insights): flag-gated Insights tab backed by a bundled pipe Aug 14, 2026
Louis Beaumont and others added 2 commits August 14, 2026 08:33
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
louis030195 force-pushed the claude/insights-tab-v1 branch from 9c16d14 to e2afcd7 Compare August 14, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant