Skip to content

fix(ui+tray): tool quarantine diff as side-by-side before/after#391

Merged
Dumbris merged 5 commits into
mainfrom
fix/ui-tool-diff-before-after
Apr 17, 2026
Merged

fix(ui+tray): tool quarantine diff as side-by-side before/after#391
Dumbris merged 5 commits into
mainfrom
fix/ui-tool-diff-before-after

Conversation

@Dumbris

@Dumbris Dumbris commented Apr 17, 2026

Copy link
Copy Markdown
Member

Summary

When a tool's description changes on an upstream server, both the web UI and the macOS tray's Tool Quarantine panel rendered the current description as a plain paragraph and, just below it, a single diff box that mixed added, removed, and same tokens. This made it look like the same text was being shown twice with random green highlights — users couldn't tell what actually changed and assumed the "changed" flag was spurious.

This PR fixes both surfaces to use a side-by-side before/after layout driven by a longest-common-subsequence word diff:

  • Before (approved) — the previously approved description, with any removed words highlighted red.
  • After (current) — the new description, with added words highlighted green.

Same-tokens render plainly in both boxes so the eye can anchor, but each side only contains its own words.

Changes

Web UI (frontend/src/views/ServerDetail.vue):

  • Splits the quarantine-panel diff into two labelled boxes
  • Suppresses the redundant {{ tool.description }} paragraph when a diff is available (previously caused the text to appear duplicated above the diff)

macOS tray (native/macos/MCPProxy/MCPProxy/Views/ServerDetailView.swift):

  • Ports the same LCS word-diff to Swift (computeWordDiff, ToolDiffPart)
  • Replaces diffLine(isOld:) with diffBeforeAfter(previous:current:) rendering AttributedString spans so removals/additions highlight inline inside their respective boxes
  • Applies to both description and schema diffs
  • Adds MCPProxyTests/ToolDiffTests.swift covering identical inputs, empty sides, the real gcore short→docstring expansion, merging of consecutive same-kind parts, and the reconstruction invariant (before = same + removed, after = same + added)

Context

Triggered by the gcore-mcp-server case: the server upgraded its MCP metadata and tool descriptions went from one-liners like "Get the list of Alibaba Cloud regions." to full docstrings documenting new limit / offset pagination params (schemas also gained those properties). These are genuine changes — the hash detector, backend, and original diff component all worked correctly — but the UI made the real diff illegible, which the user reported as "text is the same, so no actual changes."

Test plan

  • Rebuilt frontend (make build) — no TypeScript errors, bundle produced
  • Started ./mcpproxy serve against existing DB with live gcore-mcp-server changed-tool records
  • Loaded /ui/servers/gcore-mcp-server in Chrome and inspected multiple changed tools (cdn_cdn_resources_ls, cdn_list_alibaba_regions, cdn_list_aws_regions, cdn_logs_uploader_configs_ls) — each now shows a clean "Before" box with the old description and an "After" box with only the new Args docs highlighted in green
  • Verified that pending tools (no previous_description) still render the plain description paragraph as before
  • Rebuilt macOS tray binary (swiftc against macOS 13 SDK) — clean compile, binary contains BEFORE (APPROVED) and computeWordDiff symbols
  • Ran standalone Swift test harness covering all computeWordDiff invariants (identical, empty before, empty after, real gcore case, reconstruction, merging) — 17/17 passed
  • Manual macOS tray visual verification (unit tests cover the algorithm; SwiftUI rendering of the same AttributedString approach matches existing annotation badges in the tray)

🤖 Generated with Claude Code

The quarantine panel previously rendered the current description as a
plain paragraph and, below it, a single diff box that mixed added,
removed, and same tokens — making it look like the same text was being
shown twice with random green highlights. Split the diff into two
labelled boxes so each side only renders its own words: the "Before
(approved)" box shows the previous description (with any removed words
highlighted red), and the "After (current)" box shows the new
description (with added words highlighted green). The plain paragraph
is suppressed when a diff is available so the current text is not
duplicated above the "After" box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 17, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: bcc25ef
Status: ✅  Deploy successful!
Preview URL: https://0f805aa5.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-ui-tool-diff-before-afte.mcpproxy-docs.pages.dev

View logs

@github-actions

github-actions Bot commented Apr 17, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/ui-tool-diff-before-after

Available Artifacts

  • archive-darwin-amd64 (26 MB)
  • archive-darwin-arm64 (23 MB)
  • archive-linux-amd64 (15 MB)
  • archive-linux-arm64 (13 MB)
  • archive-windows-amd64 (26 MB)
  • archive-windows-arm64 (23 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (19 MB)
  • installer-dmg-darwin-arm64 (17 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 24579082185 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

Bring the macOS tray's Tool Quarantine diff in line with the web UI's
new before/after layout. Previously the tray stacked a red "minus"
box above a green "plus" box containing the raw old/new text, which
gave no hint about which specific words actually changed. Now the
diff section renders two labelled boxes ("Before (approved)" and
"After (current)") built from a longest-common-subsequence word diff;
each side only shows its own words with removals/additions
highlighted inside their respective boxes via AttributedString
background color.

Ports the same LCS used in ServerDetail.vue (split-on-whitespace
tokens, backtrack to build parts, merge consecutive parts of the same
kind). Adds ToolDiffTests covering identical inputs, empty sides, the
real gcore short→docstring expansion, and reconstruction invariants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dumbris Dumbris changed the title fix(ui): show tool quarantine diff as side-by-side before/after fix(ui+tray): tool quarantine diff as side-by-side before/after Apr 17, 2026
claude added 3 commits April 17, 2026 16:55
The word-level diff highlighted the entire differing token — so
"1 April" → "8 April" lit up the whole "1"/"8" tokens, and more
insidiously "version-1.2.3" → "version-1.2.4" lit up the entire
version string. Now we do a two-pass diff: word-level LCS for coarse
alignment, then for each adjacent (removed, added) pair we refine
with a character-level LCS. The result highlights only the
characters that actually changed (a single "1" vs "8", or "3" vs "4"
inside a version token), while large docstring expansions continue
to highlight cleanly at the word level because they have no paired
removed run to refine against.

Includes a safety cap (`maxChars = 1500`) that falls back to the raw
removed/added pair when a run is too long, so the O(N×M) char-level
dp table can never blow up on giant payloads.

Ports the refined algorithm to both ServerDetail.vue and
ServerDetailView.swift and extends ToolDiffTests with coverage for
"1 April" → "8 April", "version-1.2.3" → "version-1.2.4", and the
long-run safeguard.

Verified live in Chrome against hugginface/hf_doc_search (the real
"1 April" → "8 April" case): only one char is highlighted on each
side (`removedTexts: ["1"]`, `addedTexts: ["8"]`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ard section

Three related UI stability fixes:

1. **Stable server order across API polls.** `Runtime.GetAllServers`
   iterates the Supervisor StateView's `map[string]*ServerStatus`, which
   has non-deterministic iteration order in Go. Every poll returned the
   same servers in a different order, so filtered views like the tray's
   "Servers Needing Attention" shuffled every few seconds (a server
   would be first, then third, then second). Sort by name at the end of
   GetAllServers — stable alphabetical, deterministic across polls.

2. **Stable Recent Sessions order.** `Runtime.GetRecentSessions` relied
   on BBolt cursor order (newest-started-first by storage key). The
   tray's dashboard then deduped by client name and re-sorted by tool
   call count — which is zero for all sessions, so the final order came
   from `Dictionary.values` iteration and reshuffled each poll. Sort in
   the backend by `LastActivity` desc (break ties by ID), and replace
   the tray's tool-count sort with `lastActive` desc + tool-count +
   session ID as tiebreakers. Dashboard now shows sessions top-to-bottom
   by actual recency.

3. **Dashboard "Recent Tool Calls" → "Recent Activity".** The section
   filtered `recentActivity` down to tool_call/internal_tool_call types
   only, so users without any indexed tool calls saw "No tool calls
   recorded" despite having plenty of real activity (security scans,
   tool quarantine changes, OAuth events). Rename the section, widen
   the filter to all activity types except low-signal system events
   (`system_start`, `system_stop`), and rename the column header from
   "Tool" to "Event".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Activity Log used an inner `HSplitView` for its list + detail layout,
nested inside the main window's `NavigationSplitView`. When the detail panel
expanded on row click, the inner HSplitView's width demands competed with the
outer sidebar column and collapsed it below its minimum — so the sidebar's
"Dashboard / Servers / Activity Log / …" labels got truncated or hidden.

Replace `HSplitView` with a plain `HStack` that renders the detail pane only
when an entry is selected, tighten the list's fixed column widths a few pts
so the list + detail + sidebar all fit at the default 800pt window width,
and add a close (X) button in the detail header for an explicit dismissal
affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dumbris Dumbris merged commit debe26f into main Apr 17, 2026
24 checks passed
@Dumbris Dumbris deleted the fix/ui-tool-diff-before-after branch April 17, 2026 18:07
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