Setting a new base called setSide twice, so the first call kicked off a
comparison against the *old* other side. Scans run concurrently, so that stale
result could land last and win: the left had zoomed into the archive while the
right still showed the parent, which is exactly what it looked like on screen.
Both sides are now set in one go before comparing, and every scan is stamped so a
slower, older run can no longer overwrite a newer result.
Also renamed the pairing entries to mention 'set as base', since marking one side
and picking the other achieves the same thing for items whose names differ and
that wasn't obvious from the wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
Checking two blobs of JSON meant saving them to disk first, which is friction for
the most common quick comparison there is. A tab can now be a pair of editors:
paste on each side and the diff appears underneath.
It goes through the same engine as files, so JSON and XML are still compared
structurally — the kind is detected from the pasted content itself, since there's
no filename to go on. Reachable from the empty state or cmd-shift-T.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
- Set as base: both sides of a row become the new comparison, which is how you
descend into a folder pair. Works on folders and archives, not just files.
- Marking for comparison no longer refuses folders, so a folder inside a JAR can
be paired with one on disk; archive subtrees are extracted whole to do it.
- Copy left → right and right → left, from the toolbar, the context menu or
cmd-option-arrow, re-comparing afterwards. Only offered where the destination
is a real folder: an entry inside an archive has no path to write to, and the
button is disabled rather than failing silently.
- The scene is now a single Window instead of a WindowGroup: a comparison opened
from Finder joins the window already open as a new tab instead of spawning a
second window. Verified with two consecutive kotej:// opens — one window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
Archives are detected by their signature, not just their extension, so a ZIP
renamed to anything still opens as a folder — and nesting keeps going, verified
on EAR -> JAR -> renamed ZIP. Entries with clearly non-container extensions are
skipped without inflating, since checking the signature inside an archive means
decompressing, and an EAR holds thousands of .class files.
The comparison now records which mode settled each file, so Automatic shows an
icon per row (byte / text / structural) instead of leaving you to guess when it
went semantic.
Sizes are shown on both sides and highlighted when they disagree, which is often
the fastest hint of what changed.
Two files whose names don't match can now be compared: mark one side, pick the
other, and it becomes the new base in this tab or a new one. Entries living
inside an archive are extracted to a temporary file first, since they have no
path of their own — that's what lets a file inside a JAR be paired with a loose
one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
A changed line only said 'changed'. Now the engine classifies it and the view
colours it accordingly: whitespace-only (teal), comment-only (indigo) or real
content (orange), with a legend counting each kind present in the file. Comment
detection uses the syntax inferred from the extension and only claims a comment
change when the code either side of it is identical; an unknown file type claims
nothing, since hiding a real change is worse than showing a cosmetic one.
Within a changed line the differing characters are now highlighted rather than
tinting the whole row, so a 21 -> 18 reads as a one-character edit instead of
looking like a rewritten line.
The tree no longer uses List's outline, which only ever puts a disclosure
triangle on the leading column: rows are flattened with their own expansion
state, so both sides carry a chevron and the tree can be driven from whichever
side you're reading. Expand all / collapse all added to the context menu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
The previous change made both name columns fully flexible with no floor, so the
list collapsed to its minimum width and every name was truncated to "c com" /
"e...e". Columns now have an explicit minimum and the tree fills the window.
Added what a comparison tool is expected to have and this didn't:
- Search box to filter rows by name, on top of the status filter.
- Next/previous difference with a position counter, plus cmd-G / cmd-shift-G,
walking only what's actually visible.
- Comparison options popover exposing the text tolerances (whitespace, case,
blank lines, CRLF) that the engine already supported but had no way in.
- File sizes, a Size column, status shown as a coloured pill instead of plain
text, alternating row backgrounds, and a context menu with "Reveal in Finder"
(only for real files — archive entries have no path) and "Copy path".
- Compare-again button next to the sides.
Note on verification: ImageRenderer can't rasterise List/VSplitView, so the
headless snapshot only confirmed the bars fill the width and the difference
counter works; the tree layout needs a look on screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
The file diff sat in a narrow centred column with empty space around it: cells
had a fixed minWidth inside a two-axis ScrollView, and the empty/notice states
were vertically centred. Now each side takes exactly half the width, content is
anchored top-leading, and scrolling is vertical only, with long lines wrapping
(a "Wrap lines" toggle turns that off for code, remembered across launches).
The tree's right column was pinned at 220pt regardless of window size; both name
columns now share the space evenly and only the status column is fixed.
The "only differences" checkbox became a filter with the modes worth having when
reviewing a real comparison: everything, only differences, only changed files,
only on the left, only on the right, only on one side. Folders survive filtering
only when something inside them does, so no empty branches are left behind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
- Tabs: TabsModel owns one ComparisonModel per tab, so each keeps its own scan,
selection and options. A comparison arriving from Finder reuses the current tab
only while it's still empty, otherwise it opens another — an in-progress
comparison is never overwritten. Tab strip with close buttons, plus cmd-T and
cmd-W.
- Localization: UI strings are now English keys with en/es .strings for both the
app and the Finder extension, so it follows the OS language. Care was needed
where SwiftUI wouldn't localise: Text(someString) renders verbatim, so titles
became LocalizedStringKey and file names are explicitly Text(verbatim:) — data
must not be translated. Comparison mode labels are localised in the app because
the engine stays UI-free and English.
Verified: both .lproj ship inside the app and the appex (40 keys each, "Empty
comparison" -> "Comparación vacía"), and the app launches under es and en.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN