Skip to content

Give log-viewer rows a stable unique key, so messages stop stacking on top of each other#587

Merged
NiveditJain merged 3 commits into
mainfrom
fix/log-viewer-duplicate-keys
Jul 21, 2026
Merged

Give log-viewer rows a stable unique key, so messages stop stacking on top of each other#587
NiveditJain merged 3 commits into
mainfrom
fix/log-viewer-duplicate-keys

Conversation

@chhhee10

@chhhee10 chhhee10 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The bug

Opening a Codex session in the dashboard and collapsing a session divider leaves messages painted on top of each other, permanently — scrolling away and back does not clear it.

Root cause

Rows in the virtualized log were keyed:

key={entry.uuid || entry.timestamp}

baseEntry leaves uuid as "" for every CLI that writes no per-record uuid — Codex, Copilot, Cursor, Pi — so the key silently degraded to the timestamp. Those CLIs reuse timestamps freely:

one 771-record Codex session:
  distinct timestamps            650
  timestamps shared by >1 record  93   (worst: 5 records on one timestamp)

Duplicate React keys break reconciliation. In a virtualized list the fallout is worse than a console warning: orphaned DOM nodes keep their old transform and are never removed.

Measured against a real Codex transcript, after one segment collapse:

totalEls 11, distinctIndices 8
index 2 → 2 nodes
index 3 → 3 nodes  (one a message from 58 min later, ty=211 h=1751,
                    drawn across the two beneath it)

Fix

Three parts, all needed:

  1. lib/entry-keys.ts (new) — buildEntryKeys gives every entry a unique, render-stable identity: real uuid when present, collisions disambiguated by occurrence order.
  2. getItemKey on the virtualizer — it was absent. TanStack defaults to keying its size/element caches by index, so collapsing a segment shifted every index and replayed one entry's cached height onto a different entry. This misplaced rows independently of the key bug.
  3. getSegmentId uses the same map — queue-operation dividers are uuid-less even on Claude, so two sharing a millisecond collapsed into one segment.

The nested subagent list had the same duplicate key and is fixed alongside.

Why buildEntryKeys is its own module

log-entries.ts reaches fs/promises through its project-resolution imports. A value import of it from a client component pulls node:fs into the browser bundle and 500s the session page — this was hit and backed out during development. Client code may only import types from there. The module carries a comment saying so.

Also fixed

app/policies/hooks-client.tsx rendered the literal text in the policy filter placeholder — a JSX attribute string is not a JS string literal, so the escape was painted verbatim, beside a sibling input that had it right. Found by looking at the rendered page.

Verification

unit          2335 passed
e2e            313 passed
tsc / lint    clean (no new warnings)
docker        clean install from tarball, custom policy loads, exit 0

Behavioural check against the built artifact, driving real Chrome over CDP:

                    before                          after
rest                0 overlaps                      0 overlaps
expand raw JSON     0 overlaps                      0 overlaps
toggle segment      2 overlapping index pairs   →   0 overlaps
untoggle            still broken                →   0 overlaps
scroll away/back    still broken                →   0 overlaps

Claude sessions re-checked for regression (clean), and all six dashboard routes screenshot-inspected with zero console errors.

Scope notes

  • Latent since before the open-source release, but unreachable until [activity] CLI filter + viewable Codex sessions #226 routed Codex transcripts into the Claude viewer. [luv-292] fix: keep virtualizer scrollMargin in sync with layout shifts #292 fixed a different misalignment in the same component (stale scrollMargin), which is why the remaining symptom presented intermittently rather than on every load.
  • Claude sessions are effectively unaffected — 1 colliding key in a 4113-record session — but were exposed via uuid-less file-history-snapshot entries.
  • Known gap left open: EntryRow still anchors on entry.uuid, so #entry-… deep links and the copy-link button remain inert for uuid-less transcripts. Fixing that means replacing uuid-as-identity throughout the viewer including subagent deep-linking — a larger, riskier diff, deliberately not bundled here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed log viewer rows overlapping or rendering incorrectly after collapsing segments.
    • Improved stability when viewing entries with duplicate timestamps or identifiers.
    • Corrected the policy filter placeholder to display an ellipsis properly.
  • Tests

    • Added regression coverage for unique and stable log entry rendering keys.

chhhee10 and others added 2 commits July 21, 2026 18:30
Rows in the virtualized session log were keyed `entry.uuid || entry.timestamp`.
`baseEntry` leaves `uuid` as "" for every CLI that writes no per-record uuid
(Codex, Copilot, Cursor, Pi), so the key degraded to the timestamp — which those
CLIs reuse freely: one 771-record Codex session carries 93 timestamps shared by
2-5 records each.

Duplicate React keys break reconciliation, and in a virtualized list the fallout
is worse than a warning: orphaned DOM nodes are stranded at their old transform
and never removed, so an unrelated message paints over the one you are reading
and scrolling away and back does not clear it. Reproduced against a real Codex
transcript — after one segment collapse, data-index 2 had two nodes and
data-index 3 had three, one of them a message from 58 minutes later drawn across
the two beneath it.

Entries now take their identity from `buildEntryKeys` (lib/entry-keys.ts), which
prefers the real uuid and disambiguates collisions by occurrence order. The same
map is threaded into `getSegmentId` — queue-operation dividers are uuid-less even
on Claude, so two sharing a millisecond collapsed into one segment — and into the
virtualizer as `getItemKey`, which was absent entirely: TanStack defaults to
keying its size and element caches by index, so collapsing a segment shifted
every index and replayed one entry's cached height onto a different entry,
misplacing rows independently of the key bug. The nested subagent list carried
the same duplicate key.

`buildEntryKeys` lives in its own dependency-free module rather than in
log-entries.ts on purpose: that module reaches fs/promises through its
project-resolution imports, so a *value* import of it from a client component
pulls node:fs into the browser bundle and 500s the session page. Client code may
only import types from there.

Also fixes the Policies filter placeholder, which rendered the literal text
`…` — a JSX attribute string is not a JS string literal, so the escape was
painted verbatim beside a sibling input that had it right.

Latent since before the open-source release but unreachable until #226 routed
Codex transcripts into the Claude viewer. Known gap left open: EntryRow still
anchors on entry.uuid, so #entry-… deep links stay inert for uuid-less
transcripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The session log viewer now generates collision-resistant entry keys shared by segment logic, virtualization, and nested subagent rows. The Activity policy filter placeholder uses a literal ellipsis, and both fixes are recorded in the changelog.

Changes

Log viewer keying

Layer / File(s) Summary
Entry key generation and regression coverage
lib/entry-keys.ts, __tests__/lib/build-entry-keys.test.ts
Adds deterministic keys using UUID or source/timestamp identities, disambiguates collisions, and tests uniqueness and stability.
Segment and virtualization integration
app/components/raw-log-viewer.tsx, app/components/log-viewer/entry-row.tsx
Uses shared keys for segment visibility, expansion, queue dividers, virtualizer items, React rows, and nested subagent entries.

Policy filter placeholder

Layer / File(s) Summary
Placeholder and release notes
app/policies/hooks-client.tsx, CHANGELOG.md
Renders the policy filter placeholder with a literal ellipsis and records both fixes in the changelog.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: niveditjain

Poem

I’m a rabbit with keys in my burrow tonight,
Each log row now hops to a place just right.
No twins collide when the segments collapse,
And ellipses now sparkle in policy maps.
*thump* Stable rows! A tidy delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stabilizing log-viewer row keys to prevent overlapping messages.
Description check ✅ Passed The description is detailed and covers bug, root cause, fix, verification, and scope, but it omits the template’s Type of Change and checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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 added the bug Something isn't working label Jul 21, 2026

@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

🤖 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 `@app/policies/hooks-client.tsx`:
- Line 578: Add or update a unit test for the ActivityTab component to render it
and assert that the policy input exposes the exact placeholder value “filter by
policy…”.

In `@CHANGELOG.md`:
- Line 7: Correct the changelog entry’s before/after description: state that the
previous escaped representation rendered verbatim, and that the implementation
now uses the literal ellipsis character. Keep the policy filter and session
filter context and issue reference unchanged.
🪄 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

Run ID: 8fb86273-0ab1-4829-82da-eb9e11f6b1d1

📥 Commits

Reviewing files that changed from the base of the PR and between 7848787 and 478795f.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • __tests__/lib/build-entry-keys.test.ts
  • app/components/log-viewer/entry-row.tsx
  • app/components/raw-log-viewer.tsx
  • app/policies/hooks-client.tsx
  • lib/entry-keys.ts

Comment thread app/policies/hooks-client.tsx
Comment thread CHANGELOG.md
@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

[progress] Phase 0-1 complete: read existing comments (2 coderabbit threads, both resolved), full diff analysis done. CI checks: build ✅, quality ✅, test ✅, test-e2e ✅, docs ✅, OSV-Scanner ❌ (pre-existing). Proceeding to Phase 2 deep analysis and posting review.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Build & test complete. Results:

  • Build (CI): ✅ pass
  • Quality (CI): ✅ pass
  • Test default (CI): ✅ pass
  • Test e2e (CI): ✅ pass
  • Docs (CI): ✅ pass
  • Unit tests (local): 2335 passed (129 files)
  • E2E tests (local): 307 passed, 6 skipped
  • OSV-Scanner (CI): ❌ fail (pre-existing, unrelated to this PR)

Local verification with bun run test:run and bun run test:e2e confirmed all tests pass, including the 7 new build-entry-keys tests.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

🔍 Automated Code Review

📋 Executive Summary

This PR fixes a critical rendering bug where log viewer rows with duplicate keys overlapped after segment collapse. The fix introduces a stable, collision-resistant key system (buildEntryKeys) shared across React keys, segment IDs, and the virtualizer measurement cache. Also includes a small UI fix for a policy filter placeholder. Code quality is excellent — well-structured, thoroughly tested (7 new tests), and all existing 2335 unit + 307 e2e tests pass.


📊 Change Architecture

graph TD
    A[LogEntry[] entries] --> B[buildEntryKeys]
    B -->|"Map<LogEntry, string>"| C[React key: virtualRow.key]
    B --> D[getSegmentId: stable segment ids]
    B --> E[getItemKey: virtualizer cache]
    B --> F[SubagentToolCard keys]
    C --> G[No duplicate React keys]
    D --> H[No collapsed segment merges]
    E --> I[Stable height measurements]
    F --> J[Subagent rows stable]
    style B fill:#90EE90
    style H fill:#90EE90
    style I fill:#90EE90
Loading

Legend: 🟢 New | 🔵 Modified | 🟡 Breaking Change Risk


🔴 Breaking Changes

✅ No breaking changes detected. Pure bugfix — no API, DB schema, config, or dependency changes.


⚠️ Issues Found

  1. 🟡 Minorapp/components/raw-log-viewer.tsx:353getItemKey fallback to ?? index. If an entry somehow reaches visibleEntries without being in entryKeys, the virtualizer silently reverts to index-based keying — the exact bug this PR fixes. While this should never happen in practice, consider logging a warning or throwing here to catch regressions early. Currently silent degradation.

  2. 🔵 Infoapp/components/log-viewer/entry-row.tsx:43useMemo(() => buildEntryKeys(subagentEntries ?? []), [subagentEntries]) contains a redundant ?? [] inside the memo callback; when subagentEntries is undefined, the dependency doesn't change so the memo is stable regardless. No behavioral issue, just a minor readability note.


🔬 Logical / Bug Analysis

buildEntryKeys correctness: The implementation is sound. The key identity is entry.uuid || "${_source}:${timestamp}", and collisions are disambiguated with #N suffixes. Stability across rebuilds is guaranteed because the iteration order of the input array is deterministic. Edge cases handled: empty list, repeated UUIDs, cross-source collisions, large collision sets (50 entries tested).

Virtualizer integration: Using virtualRow.key as the React key instead of inline (entry.uuid || entry.timestamp) is the correct fix pattern. Adding getItemKey was necessary — without it TanStack Virtual defaults to index-based measurement caching, which independently caused row misplacement on segment collapse. Three subsystems (React keys, segment IDs, virtualizer cache) now share one identity map, eliminating the original bug from all three angles.

Module separation: lib/entry-keys.ts correctly uses import type only, avoiding the node:fs bundle issue that log-entries.ts would introduce. Good defensive comment explaining the reasoning.

Subagent fix: The subagent log in entry-row.tsx had the same (entry.uuid || entry.timestamp) key pattern and is now fixed identically using buildEntryKeys.

CHANGELOG: Comprehensive, well-written entries with PR number references.

Policy placeholder fix: Correct — JSX attribute strings ("…") are literal text, not JS string literals. The fix uses the actual Unicode ellipsis character .


🧪 Evidence — Build & Test Results

Local Unit Tests (2335 passed, 0 failed)
✓ __tests__/lib/build-entry-keys.test.ts (7 tests) 21ms
... (all 129 test files pass)
 Test Files  129 passed (129)
      Tests  2335 passed (2335)
Local E2E Tests (307 passed, 6 skipped)
 Test Files  15 passed | 1 skipped (16)
      Tests  307 passed | 6 skipped (313)
CI Checks
build      ✅ pass   2m4s
docs       ✅ pass   1m48s
quality    ✅ pass   1m6s
test       ✅ pass   1m7s
test-e2e   ✅ pass   58s
OSV-Scan   ❌ fail   (pre-existing, unrelated)

🔗 Issue Linkage

⚠️ No linked issue. The PR description itself thoroughly documents the bug, root cause, and fix. Consider creating an issue for better traceability.


👥 Human Review Feedback

No human review comments on this PR. CodeRabbit raised 2 threads (both resolved):

  • app/policies/hooks-client.tsx:578 — "Add regression test for rendered placeholder" → Resolved by CodeRabbit
  • CHANGELOG.md:7 — "Correct before/after description" → Resolved by CodeRabbit

💡 Suggestions

  1. Consider adding a component-level test for the virtualized list key integration. The buildEntryKeys unit is well tested, but there is no test verifying that the virtualized list with getItemKey + virtualRow.key actually renders without duplicate keys. A component-level test using @tanstack/react-virtual in a test renderer would be ideal for catching regressions.

  2. getItemKey fallback warning: Adding a console.warn inside the getItemKey ?? index fallback branch would surface regressions during development.

  3. Known gap acknowledged: The PR notes that EntryRow still anchors on entry.uuid for deep links — this is a reasonable scope decision and should be tracked as a follow-up issue.


🏆 Verdict

VERDICT: APPROVED


Automated code review · 2026-07-21 13:20 UTC

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review: Approved. ✅

  • All 2335 unit tests pass (including 7 new build-entry-keys tests)
  • All 307 e2e tests pass
  • CI: build, quality, tests, docs all green
  • No breaking changes
  • Two minor suggestions (non-blocking): getItemKey fallback warning, component-level rendering test
  • Both CodeRabbit threads already resolved
  • Code is clean, well-structured, and addresses the root cause comprehensively

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review: Approved. ✅

The advisory published after main's last green scan, so every branch went red
at once with no dependency change of its own — re-running main's last Supply
Chain scan on its unchanged commit reproduced the failure.

Both affected copies were transitive: 5.0.6 via minimatch@10, and three copies
of 1.1.15 via the minimatch@3 that eslint-plugin-import, -jsx-a11y and -react
still pull.

`bun update brace-expansion` is the wrong tool: it adds the package as a direct
dependency and lifts only the top-level copy, leaving the nested 5.0.6 and all
three 1.1.15 copies vulnerable. Bun also ignores npm's range-keyed
(`brace-expansion@^1.1.7`) and yarn's nested-path (`parent/child/pkg`) override
forms — resolving nothing, silently — so the two majors cannot be pinned
separately. Only the plain-key `overrides` form takes effect, as the existing
postcss/vite/undici pins show.

A single pin therefore also hands v5 to minimatch@3, which declares ^1.1.7.
That is safe in practice: the package's entire surface is one `expand()`
function whose signature is unchanged across these majors, and eslint exercises
that path directly — lint runs clean with the same five pre-existing warnings.

Verified with CI's own scanner image (ghcr.io/google/osv-scanner-action:v2.3.8)
against the updated lockfile: "No issues found", exit 0. osv-scanner.toml keeps
its zero ignored vulnerabilities.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

[progress] Phase 0-1 complete. Analyzing 3 commits since last review:

  • e96a456 fix(dashboard): give log-viewer rows a stable unique key
  • 478795f docs: fill in PR number in changelog entries
  • 84a6d87 fix(deps): pin brace-expansion to 5.0.7 to clear GHSA-3jxr-9vmj-r5cp

New since previous review: brace-expansion override, CHANGELOG updates with PR numbers. Two coderabbit threads already resolved, no human review comments. Proceeding to deep analysis and build verification.

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review test

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated Code Review

Executive Summary

This PR fixes a critical rendering bug where log viewer rows with duplicate keys overlapped after segment collapse. The fix introduces buildEntryKeys - a collision-resistant key system shared across React keys, segment IDs, and the virtualizer measurement cache. Three new commits since the last review: (1) the brace-expansion CVE pin (84a6d87) which cleared the OSV-Scanner CI gate, (2) changelog PR number completion (478795f), and (3) the original log-viewer fix (e96a456). All 11 CI checks are now green including OSV-Scanner. Code quality remains excellent.

Breaking Changes

No breaking changes detected. Pure bugfix + CVE mitigation.

Issue Found

[Minor] app/components/raw-log-viewer.tsx:353 - getItemKey fallback silently reverts to index-based keying if entryKeys and visibleEntries fall out of sync. Consider adding console.warn for defense-in-depth.

Deep Analysis

buildEntryKeys correctness: Sound implementation. Key identity uses entry.uuid or source:timestamp, collisions disambiguated by occurrence-order #N suffixes.

Virtualizer integration: Correct fix pattern using virtualRow.key + getItemKey. All three subsystems (React keys, segment IDs, virtualizer cache) share one identity map.

Module separation: lib/entry-keys.ts uses import type only to avoid node:fs bundle issue. Well documented.

Brace-expansion pin: Well-researched. bun update is wrong tool, bun ignores nested override syntax, only plain-key overrides works. Single pin to 5.0.7 is safe - one expand() function unchanged across majors, eslint exercises it directly. Verified with CI scanner locally.

lockfile: Correctly removes 4 nested vulnerable copies + deduplicates tailwindcss entries.

Evidence - CI Checks (all 11 pass)

build/docs/quality/test variants/test-e2e all pass. OSV-Scanner now passes (was failing before brace-expansion pin). buildEntryKeys has 7 tests covering all edge cases.

Human Review Feedback

No human review comments. Both CodeRabbit bot threads are resolved. Previous hermes-exosphere review approved with two minor suggestions that still apply.

Verdict

VERDICT: APPROVED

Automated code review - 2026-07-21

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review: Approved. All CI green, no breaking changes, one minor suggestion (getItemKey warning). Previous CodeRabbit threads resolved. Previous review suggestions still applicable.

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review: Approved. ✅

@NiveditJain
NiveditJain merged commit fc04e28 into main Jul 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants