Skip to content

Conversation

@ejc3
Copy link

@ejc3 ejc3 commented Nov 28, 2025

Adds the ability to compare two API requests.

Motiviation I was trying to figure out why claude's context reduces after restart. To investigate this bug: anthropics/claude-code#10161

Net result of investigation: https://ejc3.github.io/claude-restart-trajectory-analysis/

What it does

  • Click "Compare" to enter compare mode
  • Select any two requests using checkboxes
  • Click "Compare Selected" to view the diff

Screenshots

Screenshot 2025-11-27 at 10 01 50 PM Screenshot 2025-11-27 at 10 01 32 PM Screenshot 2025-11-27 at 10 01 27 PM

Compare mode with selection checkboxes

Comparison modal showing message differences

Changes

  • web/app/routes/_index.tsx: Added compare mode UI (toggle, checkboxes, sticky banner)
  • web/app/components/RequestCompareModal.tsx: New component for side-by-side comparison

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a request comparison feature to the Claude Code Monitor, enabling users to select and compare two API requests side-by-side. The feature helps developers investigate differences between requests, such as changes in context after restart.

Key Changes:

  • Added compare mode toggle with visual selection UI in the request list view
  • Created a comprehensive comparison modal showing message diffs, system prompts, and tool differences
  • Implemented a diff algorithm to intelligently compare message arrays between requests

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
web/app/routes/_index.tsx Adds compare mode state management, UI controls (toggle button, sticky banner), checkbox selection in request list, and keyboard shortcut handling for the compare feature
web/app/components/RequestCompareModal.tsx New modal component providing side-by-side comparison of two requests with expandable sections for messages, system prompts, and tools, including a custom diff algorithm for message comparison

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +103 to +112
// Consider messages similar if they share >80% of content
const shorter = Math.min(text1.length, text2.length);
const longer = Math.max(text1.length, text2.length);
if (longer === 0) return true;
if (shorter / longer < 0.5) return false;
// Simple check: if one is a prefix of the other or they're equal
return text1 === text2 || text1.startsWith(text2.slice(0, 100)) || text2.startsWith(text1.slice(0, 100));
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The similarity check logic has a potential issue. Line 107 checks if shorter / longer < 0.5 and returns false, but then line 109 only checks prefix matching with .slice(0, 100). This means two messages could be considered similar even if they differ significantly after the first 100 characters. Consider using a more robust similarity comparison, such as checking if one string contains a larger portion of the other, or using edit distance for better accuracy.

Copilot uses AI. Check for mistakes.
Comment on lines +398 to +449
{request1.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
</div>
<div>
<h5 className="text-xs font-medium text-gray-500 mb-2">Request #2</h5>
{request2.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The fallback message "No system prompt" won't be displayed when the system array is empty. The expression [].map(...) || <div>No system prompt</div> evaluates to an empty array (which is truthy), so the fallback is never rendered. Consider using a conditional check: {request1.body?.system?.length > 0 ? request1.body.system.map(...) : <div>No system prompt</div>}

Suggested change
{request1.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
</div>
<div>
<h5 className="text-xs font-medium text-gray-500 mb-2">Request #2</h5>
{request2.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
{request1.body?.system?.length > 0
? request1.body.system.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
))
: <div className="text-gray-400 text-xs">No system prompt</div>}
</div>
<div>
<h5 className="text-xs font-medium text-gray-500 mb-2">Request #2</h5>
{request2.body?.system?.length > 0
? request2.body.system.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
))
: <div className="text-gray-400 text-xs">No system prompt</div>}

Copilot uses AI. Check for mistakes.
Comment on lines +408 to +449
{request2.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Same issue as Request #1: The fallback message "No system prompt" won't be displayed when the system array is empty. Consider using: {request2.body?.system?.length > 0 ? request2.body.system.map(...) : <div>No system prompt</div>}

Suggested change
{request2.body?.system?.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
)) || <div className="text-gray-400 text-xs">No system prompt</div>}
{request2.body?.system && request2.body.system.length > 0
? request2.body.system.map((sys, i) => (
<div key={i} className="bg-yellow-50 border border-yellow-200 rounded p-2 mb-2 text-xs">
<pre className="whitespace-pre-wrap overflow-x-auto max-h-40 overflow-y-auto">
{sys.text.slice(0, 500)}{sys.text.length > 500 ? '...' : ''}
</pre>
</div>
))
: <div className="text-gray-400 text-xs">No system prompt</div>
}

Copilot uses AI. Check for mistakes.
User,
Bot,
Settings,
Clock,
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The Clock icon is imported but never used in this component. Consider removing it from the imports.

Suggested change
Clock,

Copilot uses AI. Check for mistakes.
Comment on lines 2 to 21
import {
X,
GitCompare,
Plus,
Minus,
Equal,
ChevronDown,
ChevronRight,
MessageCircle,
User,
Bot,
Settings,
Clock,
Cpu,
Brain,
ArrowRight
} from 'lucide-react';
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Unused import Clock.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

ejc3 added 7 commits December 20, 2025 11:45
- Replace sequential regex replacements with single-pass tokenizer in
  CodeViewer.tsx highlightCode() function
- The old approach applied patterns sequentially, causing later patterns
  to match numbers inside class attributes (e.g., "400" in "text-purple-400")
- New approach: build combined regex, iterate matches once, escape HTML
  on matched tokens only
- Also fix escapeHtml in formatters.ts to not use document.createElement
  (fails during SSR) and simplify formatLargeText to avoid over-formatting
- Add quote escaping to escapeHtml in CodeViewer.tsx for XSS protection
- Fix string regex patterns to properly handle escaped quotes
- Use template literal for paragraph wrapping in formatLargeText
- Add vitest and tests for escapeHtml, formatLargeText, and string patterns
- Change combined "X tokens" to separate "X in" / "Y out" display
- Makes it clearer how many tokens are uploaded vs generated
- Helps users understand conversation growth per turn
- Show total input tokens (cached + non-cached) instead of just non-cached
- Change cache display from absolute number to percentage
- "68,446 in 100% cached" instead of "1 in 153,525 cached"
Add guard to ensure denominator is non-zero before calculating the
cache percentage. This prevents NaN when both input_tokens and
cache_read_input_tokens are 0.
- Add Compare button in header to enter compare mode
- Allow selecting 2 requests via checkboxes for side-by-side comparison
- Create RequestCompareModal component with:
  - Summary stats (added/removed/modified/unchanged messages)
  - Side-by-side request metadata comparison
  - Message diff view with color-coded changes
  - System prompt comparison
  - Tools comparison (added/removed/common)
- Sticky compare mode banner that persists while scrolling
- Button label changes based on state (Compare / Exit Compare)
RequestCompareModal:
- Add text diff view with side-by-side line comparison (LCS algorithm)
- Show system prompt and tools in diff, not just messages
- Add size breakdown: system prompt, tools, messages in KB
- Show cache read/creation tokens separately
- Add message size (KB) to each message row in structured view
- Add download options: .diff, .json, .md formats
- Add "Side-by-Side" export for external diff tools
- Toggle between Structured and Text Diff views

_index.tsx:
- Fix cache display to only show when > 0
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