Skip to content

feat(ui): a reviewable surface for the index anomalies, not a run-on toast (GH #88) - #93

Merged
samkeen merged 2 commits into
mainfrom
claude/github-issue-81-x182uh
Jul 26, 2026
Merged

feat(ui): a reviewable surface for the index anomalies, not a run-on toast (GH #88)#93
samkeen merged 2 commits into
mainfrom
claude/github-issue-81-x182uh

Conversation

@samkeen

@samkeen samkeen commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

#81 shipped the anomaly diagnostic and rendered it as one space-joined paragraph
in a 4.5s toast. On a real vault (1122 notes, three unrelated duplicate b2ids)
that is six file paths run together in a sentence, none of them reachable, gone
before it can be read, and unrecoverable afterwards. Detection was already right;
only the desktop's rendering of it changes here.

The split: a toast is the right ping and the wrong home.

  • ui/src/anomalies.ts (new, pure + node-tested) — anomalySummary is the ping,
    one line under 80 chars naming counts and the chord, no paths; anomalyRows
    is the structured review data; anomalyKey fingerprints the outstanding set.
  • A ⚠ badge in the top bar, in the persistent shell so it keeps its identity
    across repaints. It carries the count and survives the toast, which is the
    "where did that go" answer. Cleared by the next clean pass — not dismissed,
    re-derived, so it tracks the vault rather than a notification history.
  • A review panel (⇧⌘A, or the badge): one card per anomaly with what happened,
    which files, and the fix. Every path is a control — Open for a file that still
    holds its row, Copy path for a shadowed copy, which has no row and therefore
    no file-tree entry either, so a reveal there would be a button that does
    nothing. That last point corrects a claim the design docs and both doc-comment
    mirrors made ("stays on disk and in the tree"); it never did.
  • auto-index-on-open now surfaces its findings too. It stays toast-free (the
    user didn't ask for the run), but it was dropping its report entirely — and
    app launch is exactly when last week's Finder duplicate first gets indexed.
  • The fs-watch pulse pings only when the anomaly set changes. Pulses fire on
    every external save; re-toasting an unresolved collision each time is noise
    the badge already carries.

Nothing is stored (index-engine.md §8, S2): the panel paints the latest report,
a restart shows nothing until the next pass re-derives the same anomaly, and no
anomaly is ever auto-fixed (W4). K1's four obligations: the badge is a real
button, ⇧⌘A is in the ? sheet, and the panel inherits the overlay focus trap.

Two defects caught by rendering the panel and looking at it: the collision
sentence didn't agree in number, and direction: rtl on a path (the trick
.vault-root uses to keep a tail visible) reorders neutral characters at the
string edge — __DailyNotes/x.md displayed as DailyNotes/x.md__. A path that
reads wrong and pastes right is worse than one that wraps.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01DshRBA8wRioLafsoQszxCw

Summary by CodeRabbit

  • New Features

    • Added an anomaly warning badge showing issues from the latest index pass.
    • Added an Anomalies review panel with details for duplicate and restamped notes.
    • Added actions to open indexed notes or copy unavailable paths.
    • Added the ⇧⌘A keyboard shortcut and Escape-to-close support.
    • Anomaly state now resets when switching vaults.
  • Documentation

    • Clarified how shadowed files appear and affect search, graphs, and file listings.
  • Tests

    • Added coverage for anomaly summaries, rows, actions, rendering, and state changes.

…toast (GH #88)

#81 shipped the anomaly diagnostic and rendered it as one space-joined paragraph
in a 4.5s toast. On a real vault (1122 notes, three unrelated duplicate b2ids)
that is six file paths run together in a sentence, none of them reachable, gone
before it can be read, and unrecoverable afterwards. Detection was already right;
only the desktop's rendering of it changes here.

The split: a toast is the right *ping* and the wrong *home*.

- ui/src/anomalies.ts (new, pure + node-tested) — `anomalySummary` is the ping,
  one line under 80 chars naming counts and the chord, no paths; `anomalyRows`
  is the structured review data; `anomalyKey` fingerprints the outstanding set.
- A ⚠ badge in the top bar, in the persistent shell so it keeps its identity
  across repaints. It carries the count and survives the toast, which is the
  "where did that go" answer. Cleared by the next clean pass — not dismissed,
  *re-derived*, so it tracks the vault rather than a notification history.
- A review panel (⇧⌘A, or the badge): one card per anomaly with what happened,
  which files, and the fix. Every path is a control — Open for a file that still
  holds its row, Copy path for a shadowed copy, which has no row and therefore
  no file-tree entry either, so a reveal there would be a button that does
  nothing. That last point corrects a claim the design docs and both doc-comment
  mirrors made ("stays on disk and in the tree"); it never did.
- auto-index-on-open now surfaces its findings too. It stays toast-free (the
  user didn't ask for the run), but it was dropping its report entirely — and
  app launch is exactly when last week's Finder duplicate first gets indexed.
- The fs-watch pulse pings only when the anomaly set *changes*. Pulses fire on
  every external save; re-toasting an unresolved collision each time is noise
  the badge already carries.

Nothing is stored (index-engine.md §8, S2): the panel paints the latest report,
a restart shows nothing until the next pass re-derives the same anomaly, and no
anomaly is ever auto-fixed (W4). K1's four obligations: the badge is a real
button, ⇧⌘A is in the `?` sheet, and the panel inherits the overlay focus trap.

Two defects caught by rendering the panel and looking at it: the collision
sentence didn't agree in number, and `direction: rtl` on a path (the trick
`.vault-root` uses to keep a tail visible) reorders neutral characters at the
string edge — `__DailyNotes/x.md` displayed as `DailyNotes/x.md__`. A path that
reads wrong and pastes right is worse than one that wraps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DshRBA8wRioLafsoQszxCw
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@samkeen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d6fba1ca-889d-461e-a3bf-89541adbef32

📥 Commits

Reviewing files that changed from the base of the PR and between 232b169 and 38983e7.

📒 Files selected for processing (3)
  • ui/src/main.ts
  • ui/src/render.test.ts
  • ui/src/render.ts
📝 Walkthrough

Walkthrough

The change replaces transient anomaly notices with persistent anomaly state, structured collision and restamp rows, a warning badge, and an actionable review modal. Projection reconciliation forwards reports to state adoption, while tests and documentation cover anomaly shaping and shadowed-file behavior.

Changes

Anomaly review surface

Layer / File(s) Summary
Anomaly data model and contracts
ui/src/anomalies.ts, ui/src/anomalies.test.ts
Defines anomaly types and pure helpers for counts, rows, fingerprints, and summaries, with coverage for collision and restamp semantics.
Projection reporting and anomaly state
ui/src/reconcile.ts, ui/src/state.ts, ui/src/main.ts, ui/src/reconcile.test.ts
Forwards projection reports through reconciliation, adopts anomaly state across indexing flows, and resets it when switching vaults.
Badge, modal, and interaction flow
ui/src/main.ts, ui/src/render.ts, ui/src/render.test.ts, ui/src/shortcuts.ts, ui/style.css
Adds the anomaly badge, review modal, open/copy actions, keyboard and overlay handling, styling, and rendering tests.
Shadowed collision documentation
crates/b2-core/src/ingest.rs, docs/design/index-engine.md, ui/src/types.ts
Clarifies the indexed and file-tree visibility of shadowed collision claimants.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • AlteredCraft/B2#73 — Shares the reprojectThenList reconciliation orchestration and related tests.
  • AlteredCraft/B2#84 — Directly relates to moving anomaly formatting into anomalies.ts and replacing anomalyNotice.
  • AlteredCraft/B2#89 — Shares overlay focus, keyboard shortcut, and anomaly-panel integration.

Suggested reviewers: claude

Poem

I’m a rabbit with a warning to show,
“⇧⌘A” makes anomaly petals grow.
Shadowed paths hide, but copy is near,
Kept notes open bright and clear.
Bounces and badges now guide the way—
Hop through the index review today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main UI change: a reviewable anomalies surface replacing the prior toast.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/github-issue-81-x182uh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/main.ts (1)

2873-2894: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Vault-switch race can attribute a stale vault's anomalies to the newly opened vault.

reconcileExternalChange never captures a startedRoot, and its onReport callback applies adoptAnomalies(report) unconditionally. Every other call site that awaits api.project() guards against a concurrent vault switch before touching state — e.g. doReindex explicitly bails with "If a switch already committed (vaultRoot changed), it owns the UI — bail. (A late-finishing project is harmless host-side..." and autoIndexOnOpen does the same at line 2063. If switchVault() commits while this pulse's api.project() is in flight, the old vault's onReport fires after state.anomalies was already reset to null for the new vault (line 1880), repopulating state.anomalies/the badge/panel with paths from the vault the user just left — and openFromAnomaly/copyAnomalyPath would then act on paths that aren't in the active vault.

🔒 Proposed fix — guard onReport the same way doReindex/autoIndexOnOpen do
 async function reconcileExternalChange(): Promise<void> {
   if (state.vaultRoot === null) return;
+  const startedRoot = state.vaultRoot;
   await reprojectThenList({
     reindexing: state.reindexing,
     project: api.project,
     list: loadNotes,
     onReport: (report) => {
+      if (state.vaultRoot !== startedRoot) return; // a switch took over — its own pass owns anomalies now
       const { summary, changed } = adoptAnomalies(report as IndexAnomalies);
       if (summary && changed) flash(summary);
     },
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/main.ts` around lines 2873 - 2894, Capture the current vault root at
the start of reconcileExternalChange and guard the onReport callback with it
before calling adoptAnomalies. If state.vaultRoot has changed while
reprojectThenList was awaiting api.project, ignore the stale report; otherwise
preserve the existing anomaly adoption and flash behavior.
🧹 Nitpick comments (1)
ui/src/main.ts (1)

299-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the exported REVIEW_CHORD instead of hardcoding "⇧⌘A".

anomalies.ts explicitly documents the risk here: "The chord that opens the review panel — quoted in the ping, so the toast tells you how to get the detail back after it clears. Its row in the ? sheet (shortcuts.ts) and its wiring (main.ts) spell the same chord; keep the three in step." The badge title still spells it out literally instead of importing the constant, defeating that safeguard.

♻️ Proposed fix
-import { anomalyCount, anomalyKey, anomalySummary, type IndexAnomalies } from "./anomalies";
+import { anomalyCount, anomalyKey, anomalySummary, REVIEW_CHORD, type IndexAnomalies } from "./anomalies";
-  badge.title = `${what} from the last index pass — review (⇧⌘A)`;
+  badge.title = `${what} from the last index pass — review (${REVIEW_CHORD})`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/main.ts` around lines 299 - 319, Update paintAnomalyBadge to import
and use the exported REVIEW_CHORD constant when constructing badge.title,
replacing the hardcoded “⇧⌘A” text while preserving the existing review message
and aria-label behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/main.ts`:
- Around line 707-711: Update openSettings so it cannot open while anomalies is
active: either guard the invocation with currentOverlay() === null or clear
state.anomaliesOpen within openSettings before rendering. Preserve the existing
Settings-open behavior when no overlay is active and ensure overlay/focus-trap
state is refreshed after the change.

In `@ui/src/render.ts`:
- Around line 1365-1368: Update the anomaly rendering around anomalyRows so
anomalies === null produces a distinct “no index pass yet” message instead of
the clean-pass message. Preserve the existing anomaly list and clean-pass empty
state for non-null anomaly results, including the current fallback for undefined
anomalies.

---

Outside diff comments:
In `@ui/src/main.ts`:
- Around line 2873-2894: Capture the current vault root at the start of
reconcileExternalChange and guard the onReport callback with it before calling
adoptAnomalies. If state.vaultRoot has changed while reprojectThenList was
awaiting api.project, ignore the stale report; otherwise preserve the existing
anomaly adoption and flash behavior.

---

Nitpick comments:
In `@ui/src/main.ts`:
- Around line 299-319: Update paintAnomalyBadge to import and use the exported
REVIEW_CHORD constant when constructing badge.title, replacing the hardcoded
“⇧⌘A” text while preserving the existing review message and aria-label behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cad1d1e-7067-4681-a6c0-4cb20fbd7f6f

📥 Commits

Reviewing files that changed from the base of the PR and between 2b43a9f and 232b169.

📒 Files selected for processing (13)
  • crates/b2-core/src/ingest.rs
  • docs/design/index-engine.md
  • ui/src/anomalies.test.ts
  • ui/src/anomalies.ts
  • ui/src/main.ts
  • ui/src/reconcile.test.ts
  • ui/src/reconcile.ts
  • ui/src/render.test.ts
  • ui/src/render.ts
  • ui/src/shortcuts.ts
  • ui/src/state.ts
  • ui/src/types.ts
  • ui/style.css

Comment thread ui/src/main.ts
Comment thread ui/src/render.ts Outdated
… pass that never ran (PR #93 review)

Two findings from CodeRabbit, both confirmed against the code.

- `openAnomalies` cleared `settingsOpen` but not the reverse, so ⌘, over the
  panel (that handler has no `currentOverlay()` guard) left both flags set:
  `baseOverlay` reported "settings" while `anomaliesOpen` stayed true, and
  closing Settings revealed the panel again — an accidental two-layer stack,
  which only the `?` sheet is meant to have. `openSettings` now closes the
  panel, which is also what the comment on `baseOverlay` already claimed.

- The panel rendered `anomalies === null` as "The latest pass found none."
  That is worse than the review's framing: `null` is the *ordinary* state of a
  healthy vault, since auto-index-on-open returns early when the index is
  already complete — so a session that opens a good vault and touches nothing
  never runs a whole-vault pass, and the panel was vouching for a vault nothing
  had looked at. That is the silent-shadowing failure #81 exists to end, so the
  two states now say different things and the un-run one names the way to run
  a pass. Pinned by a render test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DshRBA8wRioLafsoQszxCw
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.

2 participants