Skip to content

feat(frontend): add the Friction Log page - #2484

Merged
wesm merged 16 commits into
mainfrom
joi/03-friction-page
Oct 29, 2026
Merged

wesm merged 16 commits into
mainfrom
joi/03-friction-page

Conversation

@wesm

@wesm wesm commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Adds a Friction Log page at /friction, so people can see where their agent sessions went wrong on a given day without reading the Markdown digest. The page shows one day's digest in the order a reader needs it:

  • A count per kind: corrections, errors, workarounds, deferrals, patterns, frustration, and interruptions.
  • Needs attention: tools that failed across several sessions, then new and recurring patterns, each with how often it has happened and a link to its first occurrence that day.
  • The findings, grouped by kind. Each row leads with what happened: the quoted correction, the tool and its error, or the named pattern with its evidence. The second line names the session by its title, project, and agent, and links to the exact message.

Kinds with no findings are hidden, and a day where nothing was flagged shows a single empty state. The session signal panel lists the open session's findings with the same wording, and Quality links to the page. Navigation follows friction_readable, so a server that stores digests but cannot build them still shows the page.

A digest with findings: counts, Needs attention, and corrections

Lower sections: errors, workarounds, deferrals, patterns, frustration, and interruptions

Dark theme Nothing flagged Phone width
Dark theme A day with no findings Phone width

Screenshots use the e2e fixture archive with synthetic session titles.

To name sessions, the digest response gains a sessions map: agent, project, custom name, and the start of the first message for every session the digest's findings and P0 alerts name. It goes through the store's GetSession, so SQLite and PostgreSQL hubs both answer it. Sessions no longer in the archive fall back to their ID.

Users can step between dates, copy or download the stored Markdown, and choose Build now when the server has a friction runner and accepts builds from this caller, which /api/v1/version reports as friction_build_allowed. That holds on pg serve, which reports read_only for sessions, but not behind a proxy on a server without auth, where the run route answers 403; there the empty state says to open the page on the machine that runs AgentsView or turn on require_auth.

The URL owns the selected date (/friction?date=YYYY-MM-DD). The page follows history and shared links while it is open, and writes back the digest it settles on after the first successful list load, so a failed load cannot replace the requested date. Digest links from the API and MCP use the same form. The session findings list scrolls inside a fixed height, shows "500+" when it holds a partial list, and rechecks every minute while the tab is visible, because review runs in the background after sync.

This page shows only what main stores, so it has no spend or persona panels. When review is off, the empty state tells users to set [friction] enabled = true. The Markdown download reports a new friction_markdown value for the export_run telemetry event, and page views report friction for screen_viewed; the allowed lists and the configuration docs are updated to match.

About half the diff is component and store tests, and the message keys are in all eight locales. Row rendering lives in components/friction/FrictionFinding.svelte and FrictionSessionRef.svelte; page state is in frontend/src/lib/stores/friction.svelte.ts. The release screenshot capture is left for the release screenshot pass, because the docs build fails when the assets branch lacks a listed image.

Part 3 of 5. Rebuilt from #1937 by @salmonumbrella.

🤖 Generated with Claude Code

@roborev-ci

roborev-ci Bot commented Oct 27, 2026

Copy link
Copy Markdown

roborev: Combined Review (ad4eb36)

Verdict: Changes require fixes for 8 findings.

Medium

  • frontend/src/lib/components/friction/FrictionPage.svelte:37: Published digest links use /friction/YYYY-MM-DD, but the page reads only ?date=. Links to older digests therefore display the latest digest.

    Fix: Parse the date path segment or consistently update all link producers to the supported format.

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:106; frontend/src/lib/components/friction/SessionFrictionFindings.svelte:129: The findings panel has no height limit or scrolling. Many findings can overflow the fixed-height, overflow-hidden content column, hiding later findings and crowding out the transcript.

    Fix: Bound the panel height and enable scrolling while preserving space for the transcript.

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:49: Only the first 500 session findings are loaded because next_cursor is ignored. Later findings become inaccessible, and the displayed count incorrectly appears complete.

    Fix: Follow next_cursor or provide pagination, and indicate when the displayed count is partial.

  • frontend/src/lib/stores/friction.svelte.ts:147: Pattern pagination silently stops at 5,000 rows. Globally ranked patterns seen since the selected date can fill that cap before the selected digest's patterns appear, incorrectly producing an empty headline.

    Fix: Fetch metadata scoped to the digest's fingerprints, or preserve unmatched digest patterns and explicitly indicate incomplete recurrence metadata.

  • frontend/src/lib/stores/friction.svelte.ts:187: Building digests while viewing an older date can select the latest digest without updating the URL, so reloads and shared links open a different digest. Route-date changes are also ignored while the page remains mounted.

    Fix: Keep the selected digest and URL synchronized, and react to route-date changes while mounted.

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:59: Findings never refresh while the same session remains open because the loading effect depends only on sessionId. Background reviews can leave the panel showing stale findings or an outdated empty state.

    Fix: Refresh findings on relevant session updates and review completion using bounded polling or existing events.

  • frontend/src/lib/stores/friction.svelte.ts:93: A pending digest-list refresh can overwrite a newer user date selection by calling selectDate() with a previously captured requestedDate, leaving the displayed digest inconsistent with the URL.

    Fix: Preserve selection changes made during the request, or disable date navigation while the request is pending.

  • frontend/src/lib/components/friction/FrictionPage.test.ts:242; frontend/src/lib/components/friction/FrictionPage.test.ts:450: The guide-link test and the heuristics-copy test only repeat an unconditional literal URL and static translated text, without exercising data-dependent behavior.

    Fix: Delete both tests while retaining conditional-state, finding-rendering, and navigation coverage.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 29s | Total: 14m4s

@wesm
wesm force-pushed the joi/03-friction-page branch from ad4eb36 to ae8b6b4 Compare October 27, 2026 20:34
@roborev-ci

roborev-ci Bot commented Oct 27, 2026

Copy link
Copy Markdown

roborev: Combined Review (ae8b6b4)

Verdict: Changes require fixes for 5 findings.

Medium

  • frontend/src/lib/components/friction/FrictionPage.svelte:48: The URL synchronization effect can overwrite the initial route date with a retained selectedDate before onMount reads it, reopening an older digest or replacing a requested date absent from the cached list.

    Fix: Capture the initial URL date before effects run and defer store-to-URL synchronization until initial loading settles. Cover reopening with a retained selection.

  • frontend/src/lib/stores/friction.svelte.ts:106: Switching dates updates selectedDate while leaving the previous digest visible during loading. Markdown reads target the new date while Download targets the displayed digest's old date, allowing users to download the wrong day's digest.

    Fix: Hide or disable digest actions until the displayed digest matches selectedDate, and bind Markdown reads to the displayed digest's date.

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:62: Session findings reload only when sessionId changes, so background review completion or new findings in an active session can leave the panel indefinitely stale, including an incorrect empty state.

    Fix: Refresh findings after background review or session updates, with bounded polling as a fallback and cancellation on unmount.

  • frontend/src/lib/components/friction/FrictionPage.svelte:20: A PostgreSQL server can advertise both read_only:true and friction_available:true. The generic read_only check hides Build now even when a writable friction runner supports the operation.

    Fix: Gate building on the friction runner capability and cover the PostgreSQL capability combination.

  • frontend/src/lib/components/friction/frictionView.test.ts:109: Tests exercise unreachable response states: missing/null subject_kind at line 109, missing message ordinal at line 121, and sortedP0(null) at line 231. Production callers and generated contracts supply a non-null subject string, a numeric or null ordinal, and an alert array.

    Fix: Remove these unreachable cases while retaining valid session/diagnostic, numeric/null ordinal, and alert-ordering coverage.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 19s | Total: 13m13s

@wesm
wesm force-pushed the joi/03-friction-page branch from ae8b6b4 to 6338d6c Compare October 27, 2026 22:31
@roborev-ci

roborev-ci Bot commented Oct 27, 2026

Copy link
Copy Markdown

roborev: Combined Review (6338d6c)

Verdict: Changes require fixes for 3 findings.

Medium

  • frontend/src/lib/stores/friction.svelte.ts:115: Switching from digest A to B leaves A visible while B loads. If A’s Markdown loads during that interval, committing B does not clear it, so A’s Markdown remains displayed and copyable beneath B’s heading.

    Fix: Cancel and clear Markdown state when the displayed digest changes, or associate Markdown with its digest date and render/copy it only when dates match. Extend the pending-selection test to verify Markdown after B finishes loading.

  • frontend/src/lib/components/layout/AppHeader.svelte:100: Friction Log navigation disappears when generation is disabled or no writable runner exists, even when stored digests remain readable. The header, mobile navigation, and Quality card check friction_available instead of friction_readable.

    Fix: Gate read navigation on friction_readable and retain friction_available for Build now. Cover readable archives without a runner in navigation tests.

  • frontend/src/lib/components/friction/FrictionPage.test.ts:235: The test “loads the requested date from the URL on mount” duplicates coverage provided by the stronger URL-precedence test at line 408, which also checks exactly one requested-date load and preservation of the URL.

    Fix: Delete “loads the requested date from the URL on mount” and retain the stronger URL-precedence test.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 13s | Total: 9m48s

@wesm
wesm force-pushed the joi/03-friction-page branch from 6338d6c to 8b50345 Compare October 28, 2026 00:01
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (8b50345)

Verdict: Changes require fixes for 1 finding.

Medium

  • frontend/e2e/friction.spec.ts:137: The version mock omits friction_readable, which AppHeader requires to display Friction Log. The primary-navigation E2E test therefore cannot find its destination and will time out.

    Fix: Add friction_readable: true to the mocked version response.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 9s | Total: 8m29s

@wesm
wesm force-pushed the joi/03-friction-page branch from 8b50345 to ac7aee5 Compare October 28, 2026 13:11
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (ac7aee5)

Verdict: Changes require fixes for 2 findings.

Medium

  • frontend/e2e/friction.spec.ts:137: The version mock omits friction_readable, which AppHeader requires to display the Friction Log tab. The primary-navigation test therefore cannot find its destination and times out.

    Fix: Add friction_readable: true to the mocked version response.

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:62: Findings reload only when sessionId changes. An open panel continues displaying stale results, including an incorrect empty state, after background review updates the session's findings.

    Fix: Refresh findings while the panel is visible through review-completion notifications or bounded polling.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 24s | Total: 45m11s

@wesm
wesm force-pushed the joi/03-friction-page branch from ac7aee5 to 626d34e Compare October 28, 2026 15:28
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (626d34e)

Verdict: Changes require fixes for 1 finding.

Medium

  • frontend/src/lib/components/friction/SessionFrictionFindings.svelte:62: The open signal panel stays stale when asynchronous friction review completes or new messages sync because its effect tracks only sessionId. An initial empty response can leave it showing “No friction findings” indefinitely.

    Fix: Refresh findings while the panel is visible using bounded polling or review-completion notifications, preserving cancellation guards. Cover findings updates without changing the session ID.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 1m12s | Total: 18m57s

@wesm
wesm force-pushed the joi/03-friction-page branch from 626d34e to 7d7d3a2 Compare October 28, 2026 16:25
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (7d7d3a2)

Verdict: Changes require fixes for 1 finding.

Medium

  • frontend/src/lib/components/friction/SessionFrictionFindings.test.ts:136: The standalone empty-findings test duplicates the refresh test's initial setup and identical empty-state assertion, adding no distinct failure detection.

    Fix: Delete the standalone empty-findings test and retain the refresh test's initial empty-state assertion.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 12s | Total: 12m49s

@wesm
wesm force-pushed the joi/03-friction-page branch from 7d7d3a2 to 67c2941 Compare October 28, 2026 18:23
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (67c2941)

Verdict: Changes require fixes for 2 findings.

Medium

  • frontend/src/lib/components/friction/SessionFrictionFindings.test.ts:136: The standalone empty-findings test duplicates the empty-state setup and assertion already covered by the background-refresh test.

    Fix: Delete the standalone empty-findings test and retain the refresh test's initial empty-state check.

  • frontend/src/lib/components/friction/frictionView.test.ts:105: The isDiagnosticSubject tests duplicate diagnostic and session subject behavior already exercised through the real helper by FrictionPage.test.ts, without adding an input boundary.

    Fix: Delete the isDiagnosticSubject describe block and its unused import.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 11s | Total: 13m14s

@wesm
wesm force-pushed the joi/03-friction-page branch from 67c2941 to ddf4de4 Compare October 28, 2026 18:52
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (ddf4de4)

Verdict: No findings at or above medium severity.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 17s | Total: 14m17s

Base automatically changed from joi/02-friction-cli to main October 28, 2026 21:47
@wesm
wesm force-pushed the joi/03-friction-page branch from ddf4de4 to b02b9f9 Compare October 28, 2026 21:47
wesm added 2 commits October 28, 2026 16:47
The Friction Log could be read only from the API, CLI or MCP. This page
shows a day's digest in the app: P0 alerts and new or recurring patterns
first, then each kind of finding with a link to the session message. It
can also build missing days when the server allows it. The session signal
panel lists the findings for the open session, and Quality links to the
page.

This is #1937 rebuilt on the CLI layer. The merged review stores a smaller
summary than the one #1937 was written against, so the spend and persona
panels are gone. Their data is not in the stored summary. The empty-state
copy now says that review is opt-in, which matches main.

The Markdown export reports a new `friction_markdown` export format to
telemetry, so these downloads are counted apart from session exports.

The screenshot capture from #1937 is left for the release screenshot pass.
Listing an image that the assets branch does not have yet would stop the
docs build from hydrating its assets.
The URL and the open digest could disagree. After a build, the page opened
the latest digest but kept the old date in the URL. Browser history did not
change the open digest. A date picked while the list was refreshing could be
overwritten. The URL now owns the selected date. The page follows URL
changes and writes back the date it settles on.

The headline now uses the patterns that come with the digest. Before, the
page paged through up to 5,000 patterns since that date, and the digest's
own patterns could fall past that cap.

Digest links now point at `/friction?date=`, the form the page reads, and
are offered only now that the page exists. The session findings list
scrolls inside a fixed height and marks a partial list with "+". Two tests
that only repeated a literal URL and static copy are removed.
wesm and others added 6 commits October 28, 2026 16:47
Reopening the page with a date in the URL could open the date kept from an
earlier visit instead, because the URL sync ran before the page read the
URL. The page now reads the URL first and syncs back only after the first
load settles.

While a newly picked date loaded, Markdown reads followed the new date but
Download followed the digest still on screen, so the two could name
different days. Markdown now follows the digest on screen.

pg serve reports read_only for sessions but can still build digests, so
Build now follows friction_available alone. Test cases for response shapes
the generated types rule out are removed.
The header, mobile navigation, and Quality card checked friction_available,
which says the server can build digests. A PostgreSQL hub without a review
runner still serves stored digests, and the page was unreachable there.
They now check friction_readable; Build now still needs a runner.

Switching digests could leave the previous digest's Markdown on screen if
it loaded while the next digest was on its way. Showing a new digest now
drops that Markdown.
Navigation now shows the Friction Log when the server can read digests, so
the mocked version response needs friction_readable for the navigation test
to find the tab.
Friction review runs in the background after sync, so a session's panel
could keep showing no findings after review found some. The open panel now
rechecks every minute while the tab is visible, keeping the current list on
screen until the new answer arrives, and stops when it closes.
vp check could not resolve a named export from the Svelte module once the
formatter wrapped the combined import past the single-line ts-ignore.
The refresh test already mounts the panel with no findings and checks
the empty-state message before new findings arrive. The standalone test
repeated that check and added no coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wesm
wesm force-pushed the joi/03-friction-page branch from b02b9f9 to 7292d12 Compare October 28, 2026 21:47
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (7292d12)

Verdict: Changes require fixes for 3 findings.

Medium

  • frontend/src/lib/components/friction/FrictionPage.svelte:104: With managed Caddy and require_auth=false, friction_available remains true and enables Build now, but humaRunFrictionReview rejects forwarded unauthenticated requests with 403.

    Fix: Expose whether the current request may build digests and use that capability to hide or disable Build now, with guidance for running locally or enabling authentication.

  • frontend/src/lib/components/friction/FrictionPage.svelte:58: If the initial date-list request fails while opening a bookmarked older digest, selectedDate remains null. Retry then loads the latest digest and overwrites the bookmarked date.

    Fix: Use the current URL date when refreshing or retrying, and cover an initial list failure followed by a successful retry.

  • frontend/src/lib/components/friction/frictionView.test.ts:106: Both isDiagnosticSubject test cases duplicate observable rendering coverage in FrictionPage.test.ts and add no distinct boundary.

    Fix: Delete the isDiagnosticSubject table test and its unused import.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 17s | Total: 14m47s

wesm and others added 2 commits October 28, 2026 16:59
When the first load of a bookmarked older digest failed, no digest was
selected yet, so Retry asked for the latest digest and replaced the
date in the URL. Retry now falls back to the URL's date until a digest
loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page test renders a diagnostic finding and checks that it has no
session link, which is the only behavior isDiagnosticSubject controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (7c650ed)

Verdict: Changes require fixes for 2 findings.

Medium

  • frontend/src/lib/components/friction/FrictionPage.svelte:105; frontend/src/lib/components/friction/FrictionPage.svelte:20: In managed proxy mode with require_auth=false, friction_available enables Build now, but forwarded requests fail humaRunFrictionReview's localhost/auth check with HTTP 403.

    Fix: Expose request-specific build permission separately from runner availability. Hide or disable Build now when unauthorized, with guidance for building locally or enabling authentication.

  • frontend/src/lib/components/friction/FrictionPage.svelte:63: When a previous visit leaves selectedDate populated, reopening /friction and encountering an initial list failure still sets settled=true. The URL effect writes the cached date into the URL, so Retry requests that date instead of the latest digest and hides newer digests after recovery.

    Fix: Preserve the initial URL/latest-date intent until the first list load succeeds, and use that intent for retries before synchronizing the cached selection to the URL.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 22s | Total: 11m26s

…led load

The store keeps the last selected date across visits. When the first
list load failed on a plain /friction visit, the page still treated the
load as settled and wrote that old date into the URL, so Retry opened it
instead of the latest digest.

The page now settles only after a list load succeeds. Until then, Retry
asks for what the page opened with: the URL's date, or the latest digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (36fd74c)

Verdict: Changes require fixes for 1 finding.

Medium

  • frontend/src/lib/components/friction/FrictionPage.svelte:110: In managed proxy mode with require_auth=false, friction_available enables Build now, but forwarded build requests fail humaRunFrictionReview's authorization check with HTTP 403.

    Fix: Expose request-specific build permission and hide or disable Build now when unauthorized, with guidance for building locally or enabling authentication.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 13s | Total: 9m39s

wesm and others added 3 commits October 28, 2026 17:46
The web app reports a screen view for every route, including /friction,
but the daemon's screen allowlist did not include it. Those events were
dropped, so Friction Log use never appeared in usage counts. The
configuration guide lists the new value with the other screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page waited for its own list load to succeed before it kept the URL
in step with the digest on screen. Build now reloads through the store,
so after a failed first load a successful build showed the latest digest
while the URL still named the old bookmark, and Refresh went back to it.

The store now counts successful list loads from any caller. The page
settles once that count rises after it mounts, whichever action loaded
the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server starts a friction build only for a direct localhost request
or when it requires auth. Behind a managed proxy without auth, the page
still showed Build now, and every click failed with a 403.

/api/v1/version now reports friction_build_allowed, computed per request
with the same check as the run route. The page shows Build now only when
it is true. When it is false and no digests exist yet, the empty state
says to open the page on the machine that runs AgentsView or to turn on
require_auth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Oct 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (56ff526)

Verdict: No findings at or above medium severity.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 11s | Total: 11m39s

wesm and others added 2 commits October 28, 2026 22:04
Digest findings and P0 alerts identify sessions only by ID. Real IDs are
long opaque strings, so a reader could not tell which conversation a
finding came from without opening it.

The digest response now carries a sessions map with each named
session's agent, project, custom name, and the start of its first
message. Sessions no longer in the archive are left out, and diagnostic
identities are not sessions. The lookup goes through the store's
GetSession, so SQLite and PostgreSQL hubs both answer it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page printed the digest's raw data. Every row led with an agent tag
and an opaque session ID. Pattern links showed the issue-filing title
format, labels appeared in snake_case with literal backticks, and P0
was unexplained jargon. An empty day rendered seven "No ... detected"
lines.

Rows now lead with what happened: a quoted correction, a tool and its
error, or a named pattern with its evidence. The second line names the
session by title, project, agent, and message. A row of counts opens
the digest. "Needs attention" replaces the P0 and headline cards, and
its pattern rows use the finding's own text. Kinds with no findings are
hidden, and a day with none shows one empty state. The disclaimer
banner becomes a docs link in the digest's meta line.

The session panel uses the same finding text in place of detector
names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Oct 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (fd9b014)

Verdict: No findings at or above medium severity.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 11s | Total: 20m44s

@wesm
wesm merged commit 3485559 into main Oct 29, 2026
39 checks passed
@wesm
wesm deleted the joi/03-friction-page branch October 29, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant