Em desenvolvimento
BetaAlterado
- Verdict dots in the client Library paint in the same frame as the tile. The badge pipeline is asynchronous end to end — scanner coalescing, an IntersectionObserver tick, then the resolution round trips — several hundred milliseconds of pop-in even fully warm. But for a resolved library the answer already exists: the app's sweep wrote a verdict for every game into the library index. Badges now paint their dot synchronously from that index the moment a tile is discovered, and the full resolution replaces it invisibly (same color, plus tooltip) when it lands. Discovery and scroll-revalidation windows dropped from 300 ms to 100 ms, and the revalidation pass runs over a registry of stamped hosts with a URL-first compare — no more whole-document query or React-fiber walk per row per scroll tick, and scrolls outside the library pane are filtered out entirely.
- Capsule artwork survives a restart cached. The daemon now prefers the previous run's port and the artwork route has its own long-lived token, so image URLs stay stable across launches and the webview's HTTP cache finally applies — every launch used to re-download the whole grid's capsules over loopback. Responses are marked immutable and answer revalidations with 304s (validated by file birth time, since the LRU prune refreshes mtimes).
- The Tools tab stopped shipping the whole storage area over CDP. Opening it ran three full-area reads (~4 MB each, warm) through the Steam tab's main thread to compute two counts and a short list; a new
storageKeysrequest answers key names from the daemon's map, and the corrections review fetches only the keys it renders. - Scan progress writes no longer carry the inventory. The run's per-flush write (every 25 games / 5 s, broadcast to every tab) shipped the full [appid, name] list each time; the inventory now lives under its own once-per-run key and the flush scales with the batch.
- Superseded detail panes stop resolving. Clicking through games quickly left every abandoned pane running its four-source resolve (twice, with the SWR pass) into detached DOM, queued ahead of the visible pane; panes now carry a dispose switch their hosts flip.
- Daily cache maintenance runs in the window, a pre-warmed singleton, instead of billing whichever Steam page loaded first for materializing megabytes on the client's main thread; the page call remains as the fallback for the app-less terminal flow.
- The IPC frame parser stopped being quadratic on big messages. It concatenated its whole buffer per TCP chunk (a message near the 8 MiB cap copied ~half a GB); it now peeks the header across chunks and concatenates once per complete frame.
- Tooltips wire once per badge (vendored fix): re-renders used to stack duplicate listener sets, so one hover rebuilt the tip several times with forced layout in between, and the orphaned closures pinned their data for the element's lifetime.
- A screenful of badges now costs a handful of storage round trips, not hundreds. Keyed storage reads landing in the same tick — the burst an IntersectionObserver releases when capsules scroll into view, 150-250 near-simultaneous lookups — are coalesced into one wire request and sliced back per caller. Whole-area reads stay unbatched.
- Storage broadcasts went on a diet. Cache-class writes — the payload end of storage, up to hundreds of KB per entry — used to fan out with their full values inlined into a
Runtime.evaluatefor every attached Steam tab and a WebSocket frame for every IPC client, even though their only cross-context consumer invalidates by key. They now travel as bare keys. The menubar shell opts out of storage-change fan-out entirely (?changes=0): it consumes three control pushes and was receiving — and parsing on its main thread — every cache write of a sweep. Change frames are encoded once per notice instead of once per client. A page no longer evicts its own warm L1 entry on every cache write it makes, which used to turn the very next read into a full round trip. And the overlay scanner no longer rescans in response to its own badge and tooltip churn (vendored fix, synced from the extension). - The storage file stops rewriting itself back-to-back. A steady write stream (a sweep) kept the multi-megabyte
local.jsonserializing and rewriting continuously; the debounce-timer flush now does one pass and re-arms, while shutdown'sflush()still drains until clean. - Sweeps stopped running the window and the daemon hot. The library grid coalesces per-game verdict announcements into one repaint (the sidebar half of the same signal already did); the sweep heartbeat is one write per beat instead of two, skipping the budget when it has not moved; the lease key is ephemeral daemon-side — it updates watchers but never arms a disk flush by itself, so an idle stretch behind an in-Steam scan no longer rewrites the multi-megabyte storage file every five seconds; the scan-liveness poll only reads storage to confirm a run that went quiet instead of re-fetching the full run every 5 s for a whole scan; and an unchanged library roster no longer rewrites the snapshot on every read.
hitNetworkis now counted at the fetch seam instead of guessed from a 250 ms wall-clock threshold, which inverted under real conditions — fast CDN fetches skipped the sweep's politeness spacing while warm reads on a loaded machine got punished with it. - Capsule artwork is cached at display size. The CDN's 600×900 portraits decoded to ~2 MB of bitmap per mounted grid tile that only ever draws ~130 CSS px wide — a 5-6× decoded-memory multiplier inside the same WebContent process macOS kills under memory pressure. Downloads now resample to at most 540 px on the long side (
sips, before the atomic publish; failure keeps the original), and a stamped one-shot backlog pass shrinks the artwork cached before this change.
Corrigido
- The shell stops spinning a queue when the daemon goes down. A pipe FileHandle with a readability handler installed is a live read source, and an EOF'd descriptor is permanently readable — after a daemon exit the handler fired on empty reads for as long as the app lived, and each backoff relaunch leaked the previous pipe pair. The handler now disarms itself on EOF and the supervisor closes both pipe ends when the process ends. External links also stopped stalling the app: all three
NSWorkspace.opencall sites (page links, the update button, the update notification) now use the asynchronous variant instead of blocking the main thread until the browser is up, and the WebKit policy handler settles before the handoff. - Removals now reach the other contexts. A removed storage key traveled as an
undefinedvalue, whichJSON.stringifysilently drops on both wires — every cross-context removal notice arrived empty, so clearing the cache or undoing a correction left other tabs serving what they had until a reload. Removals now travel as bare keys. - The menubar app no longer freezes when a Steam action runs. The "working" pulse was a
TimelineViewanchored at.nowinside theMenuBarExtralabel; every label rebuild re-anchored the schedule, its first entry was always already due, and the label host serviced it synchronously — an unbounded update loop inside a single SwiftUI flush. The main thread never returned (100% CPU, memory climbing past 12 GB, popover and quit dead) from the moment a launch or relaunch started. The pulse is now driven by a Timer publisher, whose first fire is a full interval away by construction, and the label re-renders only when the derived glyph state actually changes instead of on every daemon push. The popover's elapsed-seconds ticker keeps itsTimelineViewbut is anchored at the action's start, so its schedule is stable across pushes. A stale non-terminal action can also no longer outlive its connection: losing the daemon clears it, instead of pulsing forever on a value whose terminal event can never arrive.
Adicionado
- The desktop window speaks all 29 languages again. Everything the library-intelligence work added English-first — the Overview and its cards, the background-resolution controls, the change history, the game page, the facet chips and the corrections review — is now translated in every locale table, and the escape hatch that allowed English-first strings during the work is closed. It is closed structurally, not by promise: with the pending list empty, the string tables' type collapses back to a total record, so a future key missing from any of the 29 tables is a compile error naming each table it is missing from.
- A correction reaches every place the wrong verdict was showing. Fixing a match used to repaint only the widget you fixed it in; every other open page — the store front's overlays, a search tab, the client's own Library — kept the old game's verdict until a reload. Now the pages listen: a
choice:change arriving from anywhere, including the desktop window's detail pane, re-resolves and repaints that game's badges in place, off the caches the correcting context just filled, so the repaint costs no second round of requests. The game page's widget re-renders too. In the window, the corrected game re-enters resolution at full depth, so the grid badge, the Overview and the menubar census follow on their own. - Corrections are reviewable, one by one. They are the only thing CrosTem stores that cannot be refetched, and until now the only view of them was an exported JSON file and a delete-all button. The tools tab — in the Steam panel and in the window alike — now lists each correction by game name with what it points at, and every row has its own Undo. Undoing travels the same broadcast as correcting, so it repaints everywhere the same way.
- The live check now asserts the whole loop from outside: a correction written over IPC — indistinguishable from the window's detail pane making it — must repaint a live store overlay, and the probe restores the user's data either way.
- Filter the library by what actually stops a game. Anticheat (blocked or not), architecture, a Parallels chip beside the existing runtimes, and sorting by CodeWeavers rating or by what CrosTem has looked at least recently. Plus a filter for games whose sources all answered, which is the distinction a single verdict letter cannot carry: "nobody knows" and "we could not ask" and "a verdict landed with one source missing" are three different states and only the first is really unknown.
- A full page for one game, reached from the pane beside the grid. The peek is a glance at 340px; when it is not enough, Open gives the same thing the whole content column — which is where the per-version CrossOver table and the anticheat detail stop being a squeeze. It is the same code at two sizes rather than two implementations, because two would drift and because each one costs the same four scraped sites a request.
- Widths follow the content, not the window. The Overview was capped at a reading measure, so it showed two cards at 800px and two at 1600px — widening the window changed nothing but the gutters. A dashboard earns its columns; it now goes up to four. Settings, Tools and the onboarding keep the measure, because a label/control row stretched across 1500px is harder to follow, not easier.
- The layout asks the content column how much room it has, rather than the window. Those are different by the width of the sidebar: collapse it, and the content gains 156px that a window-sized rule could not see. So the game detail pane used to stay stacked underneath the grid at a width where it had plenty of room to sit beside it. It now unstacks the moment the content is genuinely wide enough — which is a threshold in games, not pixels: it stacks when the grid beside it would drop under three columns.
- The library keeps itself current. Background resolution used to owe only what it had never seen; now it re-checks a game once the answers behind it have aged out. What counts as aged out is what a fetch would actually touch: Steam's own entry, and — when the source is switched on — the CodeWeavers and AppleGamingWiki keys a lookup would consult, including the ones it falls through to when a saved match turns out to be thin. Reading only the saved match would have declared those games fresh forever.
- Games you buy while the window is open resolve on their own, and appear in the grid without a refresh. Games you refund leave both, index row included — an orphaned row would have kept inflating the totals in the Overview and the menubar for good.
- "What changed" on the Overview, measured against a short rolling record of the library: newly analyzed, newly playable, newly blocked, and games added or removed. Aggregates only and twelve points deep, so the record is about eight hundred bytes whether you own forty games or four thousand — and it is written when a pass finishes, never per game. On a profile that has not been through a pass yet the card says so rather than showing four zeroes.
- A "Re-check everything" button, on the Overview beside the freshness numbers that would make you want it, labelled with the number of games it covers. Twenty-five minutes of requests against four small sites is not something to ask for behind an unlabelled control. It works with background resolution switched off: that switch means no work in the background, and this is neither.
- The menubar says when a pass is running, and how much is left. It is derived from the sweep's own heartbeat, so it disappears on its own when the window that was sweeping closes — which is how a sweep normally ends, with no teardown and nothing written to say so.
- Verdicts resolve for the capsules you are actually looking at. The grid asks for what it mounts, so scrolling a library nobody has scanned paints verdicts within seconds instead of after a thirty-minute batch. A background sweep drains the rest at the scan's own pacing, and yields: to the capsules on screen, to a scan running inside Steam, and to the window being hidden.
- An Overview view, and the window now lands on it. What share of your library runs on a Mac, the verdict split behind that number, how the games run (native / Rosetta / CrossOver / Parallels), which anticheats block the most, and how old the data is. All of it off
library:index, never off the raw caches — two independent readings of the same storage is how a dashboard starts contradicting the grid it claims to summarise. npm run check:window, which drives the app window in a real browser and checks what only a rendered page can tell you.live-checkstructurally cannot reach the window — it is a WKWebView, not a CDP target — so this serves the same bundle to Chromium through the dev server instead. It needs no daemon: without one the window falls back the way a fresh install does, and the Overview check asserts the empty face rather than skipping.- A library census in the menubar popover: a green/amber/red bar in proportion, "N of M analyzed", and the two numbers worth naming — how many games are unplayable and how many are blocked by an anticheat, with the vendors ranked daemon-side. It is absent on a profile that has never been analyzed rather than showing a row of zeroes, which would read as "your library is entirely unplayable" — the opposite of the truth.
- A per-game library index (
library:index): the verdict code, when it was resolved, CodeWeavers stars and status, the last-tested CrossOver version, all four AppleGamingWiki statuses, the anticheat status and the vendors behind it, the architecture and whether it was inferred, and a bitmask of which enabled sources failed. The engine computed every one of these already and threw them away: the scan persists one byte per game, which is all its tallies need but leaves the Library unable to filter or sort on anything else. Roughly 63 KB for a thousand games, against a 4 MB cache soft limit — and outside thecache:prefix, so the vendored sweep can never reclaim it.scan:lastis untouched and stays authoritative for the scan tab's own report. storageMerge, a request that assigns entries into a stored object atomically. Several contexts will now write the index at once — the window's viewport lane, its background sweep, a scan running inside Steam — and a get-then-set across the binding silently drops whichever batch loses the race. The daemon owns storage and its handler runs to completion without awaiting, so the merge is atomic there and only there; this is the same argumentclaimOncealready makes. It stays shape-agnostic, so the store learns nothing about what an entry means, and it deliberately does not broadcast: the container is tens of kilobytes and every attached Steam tab would have it inlined into aRuntime.evaluate. A small companion key rides the ordinary change path instead, so peers still hear about it.- Packaging and release pipeline:
npm run build:dmgturns the signed app intoCrosTem-<version>.dmg(plain hdiutil, LZMA — the 216 MB app travels as ~53 MB) with an Applications shortcut, a READ ME explaining both Gatekeeper flows (13/14 right-click Open; 15+ System Settings > "Open Anyway") and a mount smoke test, plusSHA256SUMS.txt. A Release workflow builds the DMG on a macOS runner at tag push and opens a draft release with notes extracted from this file (scripts/release-notes.sh) — publishing stays a human click, and the update check cannot see drafts. Homebrew: a cask template (packaging/homebrew/crostem.rb, for the s2c52/homebrew-crostem tap) andscripts/update-cask.shto rewrite version+sha256 per release. The human checklist lives indocs/release.md. - Notify-only update checks: the daemon asks GitHub Releases (latest only — drafts and prereleases are excluded by the endpoint) at startup and daily, with an ETag so a no-change answer costs nothing against the rate limit; the outcome folds into the pushed DaemonStatus. The menubar shows "Update X available…" (opening the release page) or "Check for Updates…" (the new IPC-only
updateCheckkind), and a user notification announces each new version once. Nothing ever downloads or installs itself; every failure degrades to "no update known". - First-run onboarding in the app window, gated by the daemon's atomic claimOnce (
window:onboarded): what CrosTem does, a live Steam-connection card with Launch/Relaunch actions that turns green in front of the user, the Launch-at-Login opt-in (only under the app shell), and the honest note that updates are notify-only. - CrosTem.app (
npm run build:app→build/CrosTem.app, ad-hoc signed, ~216 MB): a native SwiftUI menubar shell (macOS 13+, no Dock icon) that supervises the SEA sidecar — spawn with--supervised, parse theCROSTEM_IPChandshake, restart on crashes with 1→60 s backoff (a crash loop stops after five and offers Retry; exit 3 reports "already running in a terminal"), and take the daemon down with the app. The menu shows the daemon's live phase, contextual Launch Steam / Relaunch Steam with CrosTem actions, a Launch-at-Login toggle (SMAppService) and Quit. "Open CrosTem…" hosts the window frontend in a WKWebView. - The window frontend is served over a
crostem://URL-scheme handler (path-confined, MIME-typed) rather thanloadFileURL: WKWebView refuses Vite'scrossoriginmodule script on afile://document — the page loads and nothing executes. Observed against the real build; the IPC server's origin allowlist already admitted the scheme. External links leave for the default browser; a narrowcrostemAppmessage handler covers the login item and revealing the data folder. - Project scaffolding for the shell:
macos/project.yml(xcodegen; the .xcodeproj is generated and gitignored),scripts/build-app.sh(clean checkout → staged sidecar + frontend → xcodebuild → outer ad-hoc sign → smoke), a Darwin-guardedswiftc -typecheckin the verify gate, anddocs/app-smoke.md— including the scriptable pre-checks used to verify the real app end-to-end (window frontend connected over the daemon's WebSocket and pulled 42 capsules through the artwork route on first open). - The SEA sidecar build (
npm run build:sea→build/crostemd): the daemon as a single universal (arm64 + x86_64) macOS executable, withdist/inject.jsand all 30 locale dictionaries embedded as SEA assets — no files beside the binary. The pipeline pins the Node runtime (22.21.1, checksums inscripts/node-sea.sha256), builds a CommonJS daemon bundle (vite.daemon.cjs.config.ts), generates one arch-free blob (no snapshot/code-cache on purpose), injects it per slice with postject (fuse read from the binary itself — it rotates between Node builds), ad-hoc signs each slice,lipos them together, re-signs, and smoke-tests--versionon both architectures (x64 via Rosetta when present). Verified live: the SEA binary attached to Steam and served the embedded payload and locales over IPC. ~215 MB before DMG compression. - Two cheap SEA guards in
verify.shand CI: package.json must never grow adependencieskey (the SEArequireresolves builtins only), and the CJS bundle + blob generation must succeed (that is where SEA regressions actually show up; the full binary build stays a pre-release step on macOS). - The app window's Library view: a capsule-artwork grid of the whole Steam library with verdict badges from the last scan, search and per-verdict filters, and a detail pane that is the game-page widget re-hosted — full per-source breakdown, candidate pickers, SWR refresh — working without a Steam page under it. The scan itself is the in-Steam panel's own scan tab embedded unchanged, so progress and results are one shared state whichever side starts it, and badge repaints ride the storage broadcast.
- Offline library: every successful inventory read persists a
library:snapshot; the new IPC-onlylibrarykind serves the live list when Steam is attached and the snapshot (marked with its date, refreshed automatically on attach) when it is not. - Artwork cache: capsules come from the Steam CDN (portrait, header fallback), cached on disk under the data dir with negative-result markers and a 500 MB LRU prune, served by a token-gated
GET /artwork/<appid>.jpgroute on the IPC server. The client side only ever names an appid. - The app window frontend (
src/window/, built todist/window/): a TS/CSS shell the upcoming macOS app hosts in a WKWebView, wearing the same--ct-*theme as everything else. Navigation: Status (the daemon's phase machine live-rendered, contextual Launch/Relaunch-Steam actions, recent activity log), Settings and Tools — the last two being the injected panel's own tab modules re-hosted unchanged, so a toggle here applies live in every attached Steam tab and vice versa. - A transport seam (
src/shared/transport.ts+src/shared/adapter.ts): the chrome shim and everything above it now speak to a registered transport instead of the CDP binding directly. The injected bundle registers the binding transport; the window registers a WebSocket transport to the IPC server (pending-map correlation, automatic reconnection with backoff, status-event replay).src/upstream/remains untouched. npm run dev:window: the window served in a normal browser against the live daemon — a/__ipcdev middleware mirrorsipc.jsonfor discovery, so the whole window UI is developable without building the app.- A local IPC surface: the daemon now hosts a token-authenticated WebSocket server on
127.0.0.1(hand-rolled RFC 6455 subset, zero new dependencies) speaking the same request envelope as the CDP binding. It is the seam the upcoming macOS app builds on: the app window's frontend, the menubar shell and a dev browser all connect through it. Discovery via aCROSTEM_IPCstdout line and a mode-0600ipc.jsonin the data dir, which doubles as a single-instance lock. Storage changes now fan out through one broadcast hub across _both_ transports, so a settings toggle anywhere updates every Steam tab and every connected window live. - Control requests over IPC only —
status,steamLaunch,steamRelaunch,recentLogs,shutdown— refused withnot-allowedwhen a Steam page tries them over the CDP binding. - A
DaemonStatusstate machine (starting/waiting-steam/steam-plain/attaching/attached) pushed to IPC clients on every transition, plus a 200-line log ring served overrecentLogs. --supervisedmode for running as the app's sidecar: a Steam running without the debugging flag becomes a reported state instead of a startup error, the daemon never launches Steam on its own, stdin closing triggers a clean shutdown, and all exits converge on one disposer chain (exit code 3 = another instance already runs).--version, answered before anything else starts — the packaging pipeline's smoke test. The version is baked in at build time frompackage.json.src/daemon/paths.ts: asset resolution with anode:seabranch, preparing the single-executable sidecar build (assets read from the binary when packed, fromdist/otherwise).- A live-check
ipccase: discovery, the status greeting, control round-trips, a storage write crossing transports in both directions against a real Steam page, and the page-side control refusal. - Badges inside the client's own Library tab: star overlays on the shelf/grid capsules and compact dots on the sidebar game list. The shell window advertises
about:blankin the target list, so the daemon now attaches-and-probeslocation.hrefto find it; appids come from the artwork URLs with a defensive React-fiber fallback, and a scroll-driven revalidation pass defends against virtualized node recycling. Toggleable in the panel's Desktop section. - A whole-library compatibility scan in the panel (new Library tab): resolves every owned game through the vendored engine — politely paced, cancellable, resumable after a page navigation, with per-verdict tallies, a filterable result list, and errors counted separately from honest unknowns. The inventory is read from the client's
appStorevia a newownedGamesbridge request intoSharedJSContext. - Initial project scaffolding: GPL-3.0-or-later license, TypeScript + Vite + Vitest toolchain, ESLint/Prettier config and the
scripts/verify.shgate, mirroring the CrosTem extension's setup. scripts/sync-upstream.shvendors the extension's compatibility engine intosrc/upstream/, recording the commit it copied so drift shows up ingit diff.- The daemon: launches or attaches to a Steam client running with
-cef-enable-debugging, watches for Store and Community tabs, and injects CrosTem into each withPage.addScriptToEvaluateOnNewDocument. - The bridge: a
Runtime.addBindingchannel standing in for the extension'schrome.runtime.sendMessage. External fetches run in Node — no CORS — behind the vendored allowlist, circuit breaker, concurrency queue and retry policy. - Page-side shims for the five
chrome.*entry points the engine uses, so every module insrc/upstream/runs unmodified: storage on disk, locale dictionaries served over the binding, andrun_at: document_idletiming reproduced. - The game-page widget on
store.steampowered.com/app/*inside the client, with all 30 locales. scripts/live-check.mjsdrives the running client through one page per surface and asserts CrosTem rendered, which is the half of the claim the unit tests cannot cover. All five surfaces — widget, search, capsules, wishlist and the community library — now pass against the client.- A settings panel injected into the Steam page, standing in for the extension's options page: all five surfaces, all three sources, CrossOver version, cache TTL and the language picker, reached from a floating launcher or from a button in the game-page widget. It reuses the vendored locale keys, so it arrives in all 30 languages, and the vendored
mergeSettingsstays the only validator. Unlike the extension it has no "Apply now" step — changes are broadcast and applied live. - A tools tab beside the settings, covering what the extension's popup does: manual CodeWeavers search, cache size and clearing, and the corrections counter with delete. Corrections export and import go through the daemon to
~/Library/Application Support/CrosTem/corrections.json— the extension's Blob download and file picker do not work inside CEF — and imports are still filtered by the vendoredsanitizeChoiceImportbefore anything is stored. - A one-line welcome the first time CrosTem runs, in place of the extension's onboarding page. The claim is made by the daemon, not the page, so several attached Steam tabs cannot each open their own.
Alterado
- The DMG opens as a real installer window. The volume now lays itself out in Finder: the app's deep navy as a backdrop with the brand mint glowing behind CrosTem.app, a dotted mint arrow toward an
/Applicationsdrop link, the CT tile as the volume icon, and the pitch baked into the art — what CrosTem is ("Which of your Steam games actually run on this Mac?"), the three verdict chips as a wink at the traffic light, "Drag CrosTem into Applications to install", and the one-line Gatekeeper hint ("right-click → Open").READ ME FIRST.txtstill ships with the full story, parked below the visible fold so the window shows exactly two things: the app and where to drop it. The backdrop is a committed SVG (packaging/dmg-background.svg) rendered to 1x/2x PNGs bynpm run bg:dmg(same headless-browser pattern as the app icon) and folded into a hidpi TIFF at package time;make-dmg.shnow drivescreate-dmg(a new brew dependency, checked like xcodegen) instead of barehdiutil, keeping ULMO compression, the checksums and a mount smoke that also asserts the layout artifacts made it onto the volume. - The runtime chips read the library index instead of re-deriving their answer from raw storage. They were doing a second, colder reading of facts the grid was already showing — and paying a 1.5 MB storage read every time any cache key moved while a chip was active. That whole path is gone.
- The daily ceiling on background resolution counts what it resolved, not what it planned to. It used to charge the whole batch before any of it ran, so closing the window after ten of nine hundred spent all nine hundred — and reopening found the same work still owed and spent it again. Two sessions of that and the ceiling bound with about twenty games actually checked. It is now charged as games finish, persisted by the pass that holds the lease.
- The window shell distinguishes a nav destination from a routable view.
ViewKeywas doing both jobs, which held only while every view had a sidebar row. Four call sites want exhaustiveness over nav things alone — the sidebar glyph, the fallback labels, the menu command map and the persisted landing view — and each would be wrong if asked to produce an icon, a label, a shortcut or a boot destination for a per-game page. Splitting them makes "the last view can never be a game page" true by construction rather than by a runtime guard, so there is no migration to write. - The boot skeleton takes the shape of the view that is actually coming. It hardcoded the Library grid, so a window restoring into Settings painted a game grid that then became a form — the placeholder was describing the wrong screen.
- Fetches are scheduled per host instead of through one global four-wide lane, and newest-first within a lane. Profiling a cold search page found 83 fetches queued behind one another, with a 41-byte AppleGamingWiki reply waiting 12.3 seconds behind CodeWeavers HTML — while CodeWeavers itself answered in 0.6 seconds. Almost all of the wait was queueing. Newest-first matters because the page enqueues in DOM order, so after a scroll the rows actually on screen sat behind everything above them. On the same page: average fetch wait 14.6 s → 0.8 s, first badge 3.4 s → 1.4 s.
- Per-host concurrency replaces the single cap of 4. CodeWeavers is scraped HTML from a small company's site and stays the most conservative of the sources at 8; AppleGamingWiki is a real API and Steam is being asked about games already on screen.
- The AreWeAntiCheatYet dataset is fetched by the daemon at startup, reduced to the five fields the page actually reads, and held in memory: 457 KB → 132 KB, and off the critical path of the first game page entirely.
- The _cached_ anticheat index halves too, 315 KB → 165 KB, via the vendored engine: it is keyed by appid, by name and by edition-stripped name, and JSON has no shared references, so an entry reachable all three ways was stored three times. That is the other half of the same problem — the reduction above cuts what arrives over the binding once, this cuts what every page reads back out of storage afterwards.
scripts/sync-upstream.shdefaults to../CrosTem-extension. The old../CrosTemdefault stopped resolving when the repos were reorganised.- The daemon reattaches instead of exiting when the Steam client goes away, so a client update — which restarts Steam — no longer silently takes CrosTem with it. It waits for the client to come back rather than relaunching it: a background process that reopens an app you just quit is not one worth running. Ctrl-C is still how you stop it.
- The debugging endpoint can be pointed elsewhere with
CROSTEM_DEBUG_HOSTandCROSTEM_DEBUG_PORT. Steam's own default of127.0.0.1:8080is unchanged, and an unusable override falls back to it rather than building a broken URL. - Desktop strings can now be added English-first during a feature phase. The 30 tables in
src/inject/panel/strings/are total records, so a single new key used to red-line the typecheck for the other 29 files — which meant no branch could add a string and stay green, and every feature commit carried a mechanical 30-file diff. Keys listed inPENDING_TRANSLATIONrelax to optional while a phase is in flight;dt()already falls back to English, so the UI stays correct, just untranslated. Emptying the array at the end of the phase collapses the type back to total, which makes the typecheck itself the completeness check — and a parity test fails on an entry that every table has already translated, so the array cannot be left stale either.
Removido
- Two window strings no code still used:
dtWinWorkinganddtWinLogs, superseded by the Steam-action stepper's per-phase labels and by the live activity feed. Removed from all 30 tables.
Corrigido
npm run live-checkreported three failures that were its own, not the product's. The tools case waited for a status line in the panel header that stopped existing when saves and tool results moved onto the shared toast stack — so it had been asserting against a selector that matched nothing. The scan case demanded exactly one button after discarding a parked run, which is only true on a profile that has never scanned; with a summary to rescan there are three, and what actually has to be gone is the parked shape. And the case that drives the client into its own Library tab navigates the store page away for good, so everything queued behind it failed on that rather than on what it was testing — it runs last now.npm run live-checkcould spin forever inside its IPC case. When it was waiting for a response and an unrelated pushed event arrived first, it put that event back on the very queue it was draining — so it shifted, rejected and re-queued the same event endlessly. Because that loop only ever awaited already-resolved promises, it starved every timer in the process, which is why even a per-case deadline could not end it. Out-of-band events are now set aside locally and returned once the wait is over, and the wait is bounded by the clock rather than by what arrives.- The library grid was broken below a 900px window. Stacking the detail pane flipped its container to a column, at which point the rule that had been aligning the two side by side started shrink-wrapping the grid to its contents instead — and its contents are absolutely positioned tiles, which measure nothing. The grid collapsed to zero width, stopped rendering entirely, and left the capsules frozen wherever a wider layout had last put them. Found by the new resize check, which walks 800 to 1600.
- Resizing the window moved you about a hundred games. The grid recomputes its column count and row height together, so the same scroll position lands on a different row — and dragging from 800 to 1600 crosses seven column changes. It now holds your place: the game you have selected if there is one, otherwise whatever is at the top, at the same distance from the top of the view. Scrolling to the bottom and widening no longer clamps the position irrecoverably either, because the grid reserves its new height before it restores the scroll.
- Dragging a window edge re-bound every visible capsule on every frame the browser reported — which, for the library, meant asking the resolver for a verdict each time. Resize passes now reposition without re-binding, and coalesce to one per frame.
- A resize that happened while the Library was on another view was invisible to it: a hidden view has no width, so the grid measured zero and did nothing. It re-measures when it comes back on screen.
- Background resolution only ever ran if you opened the Library. It was owned by that view, and views are built when you first navigate to them — but the window lands on the Overview. So a session that stayed there resolved nothing at all, and the Overview's own "Re-check everything" button was permanently hidden, because there was no controller for it to ask. Keeping the library current is an app-level job: it now starts when the inventory lands, wherever the window happens to be, and the grid is one of its consumers rather than its owner. Games bought while the window is open are picked up the same way, whether or not the Library was ever opened.
npm run live-checkcould hang indefinitely with nothing on screen. It had no timeout on either of its two HTTP calls, and Node'sfetchhas none of its own — so a debugging endpoint that accepted the connection and then stalled left the script waiting forever, before it had printed a single line. The WebSocket had no connect deadline and nocloselistener either, so a socket that died quietly did the same. Every wait is now bounded and says what it was waiting for, and each case carries its own deadline so a stall names the case that caused it.- A background pass stayed dead for the rest of the session once a scan interrupted it. The scheduler suspends the sweep while a scan inside Steam owns the network, and it does that by breaking out of its own loop — but the only things that re-enter that loop are new work arriving and an in-flight request settling. Start a scan while requests were already out and the last of them settled into a loop that broke again, with nothing left to restart it. The sweep now resumes when the scan does, without clearing the circuit breaker's backoff the way a plain resume would.
- Scrolling to a capsule the background pass already owed did not move it to the front of the queue while that pass was suspended — for a scan, for a hidden window, or for its own pacing. Raising a game's priority did not re-enter the scheduler, so the game the user was actually looking at waited for whatever happened to wake it next.
- The Library, Settings and Tools views could not be opened at all when the daemon was unreachable. Each reads storage while it builds, and a build that throws leaves the shell on whatever was showing — so three of the five views were silently unreachable offline, with the sidebar still offering them. The grid already degraded to "daemon not reachable" on its own; now the views survive long enough to say so. Found by
npm run check:windowwith the daemon stopped, which is exactly the state that harness exists to cover. - The daemon wrote the library inventory straight to storage, bypassing the broadcast, so no other context ever heard that it changed. It had no observable effect while nothing was listening; it does now that a window can resolve a newly added game without being asked. Only a genuinely changed set of appids notifies — Steam is polled often enough that treating every re-read as news would wake every attached page for nothing.
- Overlapping storage flushes could lose a write. Every flush wrote through one shared
local.json.tmp, so two in flight at once raced on it — the first rename consumed the temp file and the second failed with ENOENT, dropping whatever the loser held. Easy to hit, since a flush awaits its write while the page keeps storing. Flushes are now serialised, with a unique temp name. - The cache never swept itself.
maybeDailyMaintenance()had no caller in this project: the extension runs it on every service-worker start, and the port did not reproduce that trigger, so expired entries and the storage soft limit were never enforced andlocal.jsongrew without bound. The injected entry point now runs it after the surfaces load.
Known limitations
- Ad-hoc signed, not notarized. There is no Developer ID certificate, so Gatekeeper needs the right-click-Open (macOS 13/14) or "Open Anyway" (15+) flow that
packaging/dmg-readme.txtdocuments. - Updates are notify-only. CrosTem tells you a version exists and opens the release page; it never downloads or installs anything itself.
- Running Steam with
-cef-enable-debuggingis a real change to your security posture for the life of that session: any local process can drive the client as the signed-in user. The README explains what limits the exposure. - macOS 13+, and Apple Silicon is the tested path. The sidecar is a universal binary and its x86_64 slice is smoke-tested at build time, but the app has not been walked end to end on an Intel Mac.
- The client Library surface tracks obfuscated Steam class names and may lag a client update until its anchors are re-captured. It fails inert.