The context menu never appeared because the extension wasn't registered at all:
pluginkit didn't list it, and it says nothing about why. macOS only loads Finder
Sync extensions that are sandboxed, and the target had App Sandbox off, so the
appex was silently ignored.
It now ships with its own entitlements — sandbox plus read-only access to the
items picked in Finder, which is all it needs to read the selection and open a
kotej:// URL. The containing app stays unsandboxed, since comparing arbitrary
folders needs unrestricted file access.
Verified: pluginkit lists it as enabled and the extension is loaded by Finder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
"Cotejo" means nothing outside Spanish and is hard to pronounce elsewhere, so the
project is now Kotej: same root, but it reads and sounds the same in English and
Spanish. Renamed throughout — engine, targets, bundle ids and the URL scheme,
which is now kotej://compare.
Icon generated with Codex: two facing panels split by a seam, content lines on
each side and one amber line marking a difference — the tool's job at a glance,
still legible at 16pt.
34 tests still green; kotej:// verified end to end against the demo JARs.
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
- TextDiff: line alignment for the file view. Common prefix/suffix are matched
first, so one change in a 20k-line file costs almost nothing; one-sided runs
are paired into "changed" rows instead of a delete plus an insert, and very
large differing sections fall back to positional pairing rather than hanging.
- App: empty comparison with drop wells, drag & drop or pick per side, mode
picker (automatic per file type, or force binary/text/semantic), side-by-side
tree with archives as folders, "only differences" filter, per-file diff and a
totals bar. Scanning runs off the main actor.
- Finder extension: "Comparar los 2 elementos", or the two-step "Seleccionar
para comparar" then "Comparar con …". It holds no logic — it just opens
cotejo://compare, which the app handles.
- Engine target raised to macOS 14 for @Observable; Totals/status made public.
Verified on a real pair of JARs: reordered JSON keys and reformatted XML come out
"equivalent", genuine edits "different", and added/removed files as only-left or
only-right.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN