Releases: SysAdminDoc/Aviary
Release list
Aviary v1.49.5
Aviary 1.49.5. Download the ZIP for a self-hosted extension install.
Aviary v1.49.4
Aviary v1.49.4 makes installation easier to follow and fixes two problems found while checking the shipped packages.
- The README now starts with release downloads and current extension screenshots. Firefox's temporary installation and userscript-manager limits are explained before setup.
- Library storage no longer stays on Measuring. It counts the active profile's collections and reports unavailable measurements explicitly.
- The compact source ZIP includes the icons required for a clean build. The standalone README's image and guide links work outside a checkout.
- The original bird icon remains. All 769 original files, including 352 images, are preserved in the repository's concept archive alongside 68 review PNGs.
Use the Chromium ZIP for developer loading. Firefox remains an unsigned temporary add-on with a placeholder ID. The secondary CRX3 is self-signed, not browser-store approval. Neither route removes browser installation restrictions.
Local checks cover 1,167 unit tests, 15 visual tests and all seven browser smoke lanes. Chrome 151, Firefox 155.0.1, Tampermonkey 5.5.0 and Violentmonkey 2.47.0 were exercised in disposable headless profiles. A fresh source extraction rebuilt matching extension ZIPs and userscript files. SHA-256 checksums and a release manifest accompany the downloads.
Screenshots use synthetic X fixtures. No authenticated live-X session or production provider account was tested. The DOM reference remains May 19, 2026, with its existing September 30 freshness-waiver expiry. Back up your library before removing the extension; ordinary removal deletes its stored records.
Aviary v1.47.0
Aviary 1.47.0 makes downloading media the primary path on X. Videos and images use the best available quality, including original-image fallbacks. The redesigned Control Center, permission setup, Wide layout, and mobile feed controls use a flatter and more readable visual system. Verified locally with 862 tests, 65 visual baselines, 2,268 release combinations, and Chrome, Firefox, userscript, and provider smoke checks.
v1.45.0
Tests now import TypeScript sources directly through Node's native type stripping, with explicit .ts specifiers and isolated entrypoint imports for browser stubs. Production userscript and extension builds remain bundled. Includes refreshed v1.45.0 userscript and Chrome and Firefox extension packages.
Aviary v1.37.0
Aviary 1.37.0 gives authored X themes a 1120px Wide canvas, flat timeline surfaces, and connected conversation replies. Comfortable now uses 920px while keeping the discovery rail. Download stays visible at the edge of media action rows. Includes Chrome, Firefox, and userscript builds.
Aviary 1.36.0
Aviary 1.36.0 simplifies the Control Center and optional permissions page with a flatter, denser visual system. Feed download actions are easier to find, and late-loading video buttons stay clickable while Aviary resolves the highest-quality direct file. Includes Chrome, Firefox, and userscript builds.
v1.35.0
Added
-
The timeline can stop. Set a number of posts and the feed stops extending there, with a
control to continue that releases another page of the same size. Off by default; zero leaves X's
endless scroll exactly as it was. It applies to feeds that extend — home, profile, search — and
never to a conversation, which is finite already. The property the implementation is built around
is that nothing already on screen moves: the control is inserted after the last post shown
rather than above the cut, so continuing extends downward into space that was below the fold, and
a post the reader was looking at stays exactly where it was. A release granted on one feed does
not carry to the next. -
A filtered post can say what caught it. A filter that hides silently is hard to tell from a
bug, so a suppressed post now names the rule, keyword, pattern, engagement floor or media type
responsible — by the rule's title where it has one. The reason comes out of the same evaluation
that made the decision rather than being worked out afterwards, because a reason computed
separately is a second implementation of the filter and the two would eventually disagree about a
post the reader is looking at. Nothing is stored: the sentence lives on the article as an
attribute the stylesheet reads withattr(), and structural reasons are written into the
stylesheet itself, so no post costs anything extra. The default names reasons on dimmed posts,
which changes no layout; the third setting also turns a hidden post into a one-line strip that
opens on hover or when tabbed into — the post stays clipped rather than having its children
removed, so it remains reachable by keyboard. -
A filter rule can carry a name and a lifetime. Start a line with
[a title]to name a rule,
and give itfor 7d from <instant>to have it apply only for a while. An expired rule stops
filtering on the next pass with no settings change and no reload — and is never deleted: the
panel lists what expired and offers one control that restarts each rule's own window from now.
The window is stored as its duration plus its start rather than as a deadline, which is what
makes renewing possible without asking the user again what they originally chose. Rules written
before any of this keep parsing exactly as they did.
Fixed
-
A feature module that nothing registers is now a test failure. The boot test checked a
hand-written list of ten feature ids, so a new module could be written, wired to a setting, tested
against directly, and never loaded by the app — which is how the timeline-stop feature first
landed. The check now reads everyFeatureModuleid out ofsrc/featuresand requires the booted
registry to hold all thirty-four. -
Eleven sentences in the Control Center were shipping in English in all eight locales while
translation coverage reported 100%. The extractor draws the panel to harvest its copy, so a row
that only appears under a condition — an expired rule, a callback the harness cannot supply — is
reached only by the source scan, and that scan read one string per row: the label. Every such
row's explanatory sentence was invisible to it. The scan now reads the description too, for each
helper whose second argument is one, and a test drives the harvester over a conditional row so
the gap cannot reopen. -
Filter on the shape of a post, not only its words. Two predicates, both off by default.
Quote posts hides or dims a post that quotes another — structural, so it is a:has()rule
costing nothing per post, and it tells a quoted post from a link preview by the quoted author's
name rather than by therole="link"the two share. Low-engagement posts hides or dims a post
under a minimum number of replies, reposts or likes; the count is read from the action button's
own accessible name, which carries the exact figure where the visible text is rounded to "11K",
and which reads zero as zero where the visible text is simply empty. A post whose count cannot be
read is never filtered on it, and a minimum of zero reads no counts at all.
Changed
- The media and verified filters are stylesheet rules now, not work done per post. Both were
questions about the shape of a post's own subtree — does it contain a photo, a video player, a
GIF, the verified badge — and the engine answered them with fivequerySelectorcalls against
every article on every mutation batch. They are emitted as:has()rules instead, from one table
that the remaining JS reader shares, so a timeline scroll no longer pays for them at all. Two
consequences beyond the saved work: media that renders after a post was judged is now caught,
where the JS pass had stamped that post done and would never look at it again; and a post the
engine has not judged yet is left alone, so nothing flashes hidden before the allowlist is
consulted. Precedence is unchanged — an allowlisted author still outranks everything, a rule that
dims still beats a media rule that would hide, and hiding video still hides GIFs.
v1.34.0
1.34.0 - 2026-08-19
Changed
-
The unpacked extension bundles are no longer carried in git.
dist/extension-chrome/and
dist/extension-firefox/are build output thatnpm run verifyregenerates on every commit
touchingsrc/— 1.9 MB of incompressiblecontent.jsper target, per commit, that nothing read:
the load-unpacked instructions and the release artifacts both come from a build. They are ignored
and built on demand;dist/aviary.user.jsanddist/aviary.meta.jsstay tracked because a
userscript manager polls them by raw URL.docs/INSTALL.mdnow says to build first, for both
browsers. -
Every release named in this file now has a tag on the commit that bumped its version. Nine
releases were tagged and thirty-one were documented, so most of the project's history could not be
checked out, diffed, or bisected by reference — which is the wrong gap for a project whose most
frequent question is "when did this selector break". Twenty-four tags were added, located by the
commit that changedpackage.jsonrather than by commit subject, since not every release commit
said "release". The two releases with no entry at all, 1.7.0 and 1.15.0, are written below
from their own commit ranges and labelled as reconstructed.
Added
- Copy a post's link for an alternate front-end. Library gains a Copy post links as choice
(X, fxtwitter, vxtwitter, fixupx, xcancel); pick anything but X and each post grows a Copy
link control that writes that post's address on the chosen host. Deliberately copy-time
rewriting rather than redirection: redirectingx.comnavigation is the shape everyone else
shipped and has since removed, because logging in through the alternate host now sets anx.com
cookie and the front-ends people redirected to have been architecturally dead since X removed
guest tokens. Nothing X rendered is modified, no navigation is redirected, and no request is
originated — only what you copy changes. Off by default, and the host list is closed so a typo
cannot produce a link to somewhere you did not mean.
v1.33.0
1.33.0 - 2026-08-18
Fixed
- Two X tabs no longer erase each other's work. Every whole-state store loaded once into memory
and persisted that snapshot wholesale, so a hide in one tab and a hide in the other kept only
whichever wrote second -- silently, with nothing to see afterwards but a missing entry. Hidden
posts, seen posts, the media dedup index, the audit log and the bookmark library now merge on
write under a cross-tab Web Lock: each save folds the change it just made into what is actually
stored, never the whole in-memory list, so a deletion or a "clear" in one tab is not undone by
the other's next save either. Aria2 history, the semantic index and archive import jobs are
written under the same lock but deliberately not merged — each is derived, reconciled, or owned
by the tab running it, and the reasoning is recorded where the write happens. - A daily provider budget can no longer be spent once per open tab. The usage ledger checked
the counter and then wrote it back, which across two tabs is a time-of-check-to-time-of-use race:
both read the same "bytes used today", both concluded there was room. The whole check-and-spend
now runs inside one lock and re-reads the stored ledger first, combined with what the tab already
knows by taking the higher of the two counters — a daily total only goes up, so that can refuse a
request that would have been allowed but can never allow one that should have been refused. - A library restore holds one lock across snapshot, write, and rollback. The window between
reading a key's current value (kept for the rollback) and overwriting it was where an ordinary
save from a second tab used to land, leaving the rollback holding a value that was no longer
current. - Bookmark ids were
Date.now()plus a counter that restarts at zero in every tab, so two tabs
saving in the same millisecond produced the same id. Found by the cross-tab merge, which keys on
id and would have kept one of the two bookmarks.
v1.32.0
1.32.0 - 2026-08-18
Added
- Trust -> Find the feature breaking this page. A binary search over the features that are
actually running: it turns them all off, then back on in halves, asking after each round whether
the page is still wrong, and names the one responsible in about five rounds instead of a linear
hunt through thirty-odd settings. Turning a feature off runs its owndestroyrather than
writing a setting, so reloading restores everything however you stop -- including abandoning
mid-round. The first round turns everything off, so "this is not Aviary" is a result the search
can reach rather than a feature named at random. The Control Center and its locale stay on
throughout and can never be named. The outcome travels in Copy diagnostics as one
content-free line, and the bug report template has a field for it.
Fixed
-
"Aviary's page script did not load" no longer stands in for "something else answered it".
A script that wins the very first handshake owns the page agent -- the transferred control port
stops a later one from displacing it, but nothing can stop the first. The real bridge then sat
out a three-second timeout and reportedagent-absent, which reads as a browser compatibility
problem when what actually happened is that Aviary's default-on network ad guard is answering to
somebody else. The agent now refuses a late handshake audibly, and Trust says so. The design note
oninstallPageAgentstates plainly what the boundary defends and what it does not: it is not
cryptographic, the refusal itself is forgeable by the page, and it downgrades a diagnostic rather
than a decision. -
A browser download is only Saved once the browser says it finished.
chrome.downloads.download()resolves when the browser accepts the request, so an interrupted
transfer had already been reported as Saved, marked completed in the queue, and written into the
duplicate index -- which then refused the retry the user wanted. The extension now reports each
download's terminal state back to the tab that asked. Started and Saved are distinct
states on the button; an interrupted transfer reads Retry, marks its queue entry failed, and
is never recorded as a duplicate; and a transfer still running when the wait gives up stays
Started rather than claiming either outcome. The tracking is persisted, so the answer still
arrives after the service worker has been suspended and restarted, and a quality-fallback retry
reports under the id the page is waiting on. Userscript saves are unchanged:GM_download's own
callback already is the terminal state. -
Media inside a quoted post is the quoted account's, not the account that quoted them. The
extractor walked the wholearticlesubtree and filed every photo and player it found under the
outer post's handle and id, so saving a photo out of a quote wrote it as if the quoting account
had published it. Each asset now carries its owner. A quoted post's media is saved under the
quoted account's handle, its own text, and its own post id where the DOM or a captured record
supplies one; the post-level Download saves only the post's own media and says so, and every
excluded asset still has its own Save control. Export records mark quoted and card media with
whose it is instead of listing it among the account's own. -
A post with no permalink of its own -- a reply shell still building, or a quote-only post -- no
longer adopts the quoted post's id, handle, or text. The article's identity is read from outside
the quote it carries. -
The exporter's "this is a quoted post" test and the media extractor's are now one definition.
They disagreed: the exporter looked only for two named test ids while X's current Home renders a
quote as a focusabledivwith no test id, so a record could carry a quoted photo with no note
that a quote existed at all.
Changed
-
The browser floors are declared once, with their reason. They lived in three places that did
not know about each other -- a number in each manifest and a sentence indocs/INSTALL.md-- and
the floor is what decides whether a platform feature can be used directly or needs a detection
branch.src/extension/browser-floors.tsnow declares both, records per-feature which of the two
is true, and preflight fails the build if either manifest disagrees. The decision itself: Firefox
stays at 128. It is an ESR line, Aviary is sideloaded rather than distributed through a store,
and raising the floor to pick upURLPattern,@scopeor the Navigation API without a branch
would exclude the users most likely to be running ESR. -
FeatureRegistrygainedsuspend/resume, and now runsapplyin registration order rather
than initialization order -- the two differ only once a feature has been suspended and resumed,
and ad protection is registered first precisely so it runs before anything that reads the
timeline.