v0.4.0 (2026-07-07)
v0.4.0 is the largest release since the project was structured. Two things happen at once.
First, asset generation stops being a 2D pixel-art test and becomes a multi-medium discipline. Where v0.3.2 shipped a self-contained desktop app over a benchmark that drew small sprites, a case can now ask a model to sculpt an opaque-voxel model, extract a meshed surface three different ways (marching cubes, surface nets, dual contouring), rig a skinned character on a real skeleton, paint a high-resolution interface asset or a tileable PBR material, author a particle system, or synthesize a sound effect or a piece of music — all through the same drive-a-tool-through-many-small-steps design that makes the 2D kinds work, and each with a runtime that lets a real game consume what a run produced.
Second, the last off-cluster pieces of the pipeline come onto the cluster. A
run’s public release is now a per-publish Kubernetes Job instead of a step driven
from an operator’s own machine, the web console ships
as a pod, and with those gone azure-prod comes online as a real,
VPN-reachable production environment. Alongside, a broad wave of new game and
asset test cases, three new models, and a rebuilt catalog UX.
This remains pre-1.0 software, built first for the people working on the project.
Breaking changes
Section titled “Breaking changes”The standalone “push” run operation is gone
Section titled “The standalone “push” run operation is gone”The run lifecycle drops from three operator steps to two: review → publish. “Push” was a vestige of the long-lived-worker model — an explicit step that released a finished run and stored its record before review. In the in-cluster ephemeral-pod model there is nothing for it to do: when the driver finishes a run it reports the produced record to the backend automatically (stored privately) and uploads the run tree to the artifact service, where the build is playable for review immediately. By v0.3.1 every push surface had already been reduced to a no-op; v0.4.0 removes the dead surface end to end:
- The
tcab pushsubcommand is removed from the CLI, along with the “Push run” button in both consoles. Review and publish no longer gate on a prior push. - The backend’s
POST /runspush route is removed (the storage primitive the driver uses to record a finished run stays). The core’sPublisher::pushfamily,BackendClient::push_run, and thePushRequest/PushResponsewire types and their generated contract schema are gone; the release input type is renamedPushRequest→ReleaseRequest. - The docs now describe a run as “stored when produced” rather than pushed —
see results, terminology, and
architecture.
tcab publishremains a solo convenience that self-reviews and publishes in one shot.
The arena’s separate “pushed-run controller” concept is unrelated and unchanged.
Features
Section titled “Features”Asset generation learns a third dimension: voxel-model and voxel-animation
Section titled “Asset generation learns a third dimension: voxel-model and voxel-animation”A case declares one of two new asset_kinds to produce a 3D asset instead of a 2D
image (see Manifests):
voxel-model— a static opaque-RGB voxel model, sculpted with the newvoxelbinary.voxel-animation— a rigged, animated model, sculpted per part with the newvoxel-animbinary (a required--part <name>on every op) plus rig subcommands.
Both replace the [canvas] table with a [voxel] table (width/height/depth
and a preview background); the volume is opaque #rrggbb with no alpha and
starts empty. The binaries share one voxel-and-raster library with the
validator — including a fixed, integer-only isometric rasterizer that serves
both the in-container preview and the post-run regeneration, so
cheat-divergence is as enforceable in 3D as
it is for a sprite. See
The voxel binaries.
The rig: model-invented parts and joints, with a game-facing animation contract
Section titled “The rig: model-invented parts and joints, with a game-facing animation contract”A voxel-animation case’s [model] table declares only the required
animations — named F-curve timelines a game plays back (a walk, a recoil, an
idle), each with a loop and auto_play flag. The rig itself is
model-invented: the model devises whatever parts (a parent/child hierarchy with
attachment pivots) and joints (rotations or translations, each caller-driven
for a game to pose at runtime or auto-play from a model-authored clip) it needs
to carry those animations, creating them at run time with define-part /
define-joint. This makes the case measure whether a model can work out the pieces
a subject needs — not whether it can follow a prescribed skeleton. The validator
derives the parts to score from the produced rig.json and reconciles it against
the contract: a missing or empty required animation is a recorded, zero-scored
contract gap, not a crash. See
Voxel models and rigs and
Evaluation. A separate
walker-rigging guide covers building
legged locomotion as in-place cycles.
Meshed models: marching cubes, surface nets, and dual contouring
Section titled “Meshed models: marching cubes, surface nets, and dual contouring”Beyond the cube kinds, a case can ask for a meshed asset — a model sculpts a
signed-distance field with a shared CSG vocabulary (add-/subtract-
sphere/box/ellipsoid/cylinder, soft-min --blend, replace-color, mirror,
translate, copy) and the environment extracts it to a real triangle mesh. The
extraction algorithm is fixed by the asset_kind, and each gives the surface a
different character:
mc-model/mc-animation— marching cubes: a coarse grid, low-poly and chunky-faceted.sn-model/sn-animation— surface nets: a medium grid, smooth and uniformly dense.dc-model/dc-animation— dual contouring: a fine grid solved per cell with a quadratic error function, high-fidelity and edge-preserving (with a per-primitive--sharp/--smoothtag exposed only fordc).
The static kinds use the mc / sn / dc binaries; the animated kinds use the
-anim binaries and carry the same model-invented parts/joints rig as
voxel-animation. After extraction every binary runs a quadric-error-metric
decimation pass that collapses an edge only where it provably preserves
watertight 2-manifold topology and never crosses a color boundary or a sharp
feature. Meshing is not regenerated: the mesher runs once and the emitted
geometry is authoritative — the validator decodes it and checks the rig contract.
Rendering is an on-request render command (not automatic after every op), and
previews come from a shared wgpu renderer running on Mesa lavapipe (software
Vulkan, headless, arch-agnostic). See
The meshing binaries.
Skinned characters on a real skeleton
Section titled “Skinned characters on a real skeleton”Three further kinds — mc-skinned, sn-skinned, dc-skinned —
produce a single continuous mesh bound to a skeleton and deformed by linear-blend
skinning, rather than separate parts posed about pivots. There is no --part
flag and no static skinned kind: a skinned model is one whole-body field, and every
skinned model is inherently animated. On top of the meshing vocabulary the
-skin binaries add a bone layer (define-bone, set-bone, define-joint,
optional paint-weight), and render derives automatic bone-heat skin weights
(a deterministic function of the mesh and skeleton, capped at four influences per
vertex and normalized). The emitted .glb carries JOINTS_0 / WEIGHTS_0, a
glTF skin with inverse-bind matrices, and the bone hierarchy; a first-person
viewmodel is just a partial-body skinned rig with a weapon on an empty socket bone.
See The skinned binaries.
Two new 2D kinds: high-resolution UI and PBR materials
Section titled “Two new 2D kinds: high-resolution UI and PBR materials”Two kinds bring the 2D side up to large, high-resolution work:
uiproduces a high-resolution interface asset — a large RGBA canvas (typically 256–2048 px) painted with a full layer stack, blend modes, brushes, gradients, masks, filters, and layer effects (bevel/glow/shadow/stroke) through thepaintbinary, plus a companionuibinary for crisp composition (rects, text with baked fonts, nine-slice). Auikit emits one flattened PNG per element and aui.jsondescribing the elements, their nine-slice insets, and the atlas.materialproduces a tileable PBR material — per-channel maps (base color, normal, roughness, metallic, AO, emissive) authored with atexturebinary (the paint vocabulary restricted to one square toroidally-wrapping map, plus proceduralnoise/pattern/warp) and apbrbinary (bake-normal/bake-ao/bake-curvaturefrom a height map,assemble,render). The preview projects the material triplanar onto a sphere/cube/cylinder/plane with no UVs.
Neither kind is regenerated: the emitted PNGs and material.json / ui.json are
authoritative, judged on their well-formedness plus a reviewer’s assessment of the
rendered result. See Evaluation.
Particle systems
Section titled “Particle systems”A particle-2d or particle-3d case produces a particle system, not a
still — a model authors emitters, forces, per-particle F-curves, and sub-emitters
(add-emitter, set-forces, set-particle, add-subemitter, set-timeline)
that emit a system.json. The effect is simulated live rather than baked, so it
varies play to play; the validator judges the emitted system, not any single frame.
A new @test-cabinet/particle-runtime package (mirroring voxel-runtime)
splits into a pure-core simulator with a three billboard binding and a
canvas 2D binding, so the review UI, the live monitor, and a real game all
simulate the effect the same way. See
The particle binaries.
Audio: sound effects and music
Section titled “Audio: sound effects and music”Three kinds bring sound into the benchmark, each emitting a rendered PCM clip.wav:
sfx-synth— pure procedural synthesis: a modular voice/envelope/filter/effects op graph, no samples.sfx-sample— the synth vocabulary plus a baked sample library to mix (weapon, naval, and footstep SFX).music— a symbolic sequencer (tempo, time signature, tracks, notes) that renders both aclip.wavand a portableclip.midscore, playing real sampled instrument banks.
Audio is deterministic and regenerated from the recorded actions — synthesis
noise is fixed-seed and sample mixing is a pure function of the placed layers — so
the op log stays authoritative; the validator confirms the clip is well-formed,
within the [audio] format, no longer than max_duration_ms, and not silent.
A reviewer plays the clip against the brief with its waveform and spectrogram
(and, for music, a piano-roll) shown alongside. Sample and instrument packs
live in a private Cloudflare R2 bucket rather than the repo, pinned into each
audio image at build time by digest through a short-lived presigned URL
(combat-core for SFX, gm-lite for music); adding or updating a pack is a new
version plus an image rebuild, never an in-place edit. See
The audio binaries and
Publishing an audio sample pack.
Runtimes to consume a produced asset, shipped into a game with packages
Section titled “Runtimes to consume a produced asset, shipped into a game with packages”Several of the new kinds produce not a finished picture but data that needs a
runtime to play or pose it — a particle system.json, a voxel or meshed rig. A
new @test-cabinet/voxel-runtime package (pure-core posing math plus a
three binding) poses a produced rig for both the review viewer and real games,
and voxel-runtime grows a framework-agnostic mesh builder and a standalone
scripts/voxel-to-gltf.mjs converter that turns a run’s rig.json + per-part
.glb into a standard glTF 2.0 / GLB — one culled, vertex-colored mesh per
part, a node hierarchy matching the part tree, and one baked animation per
case-authored clip. See voxel-runtime.
To close the loop, an end-to-end case can now
declare a packages key — e.g. packages = ["@test-cabinet/particle-runtime"]
— so a built game can import a Test Cabinet runtime library to consume a produced
asset instead of reimplementing the runtime from a schema. The shippable set is an
allowlist; at seed time the declared libraries are vendored into the run repo
under .tcab/packages/ and committed, and the case ships a workspace package.json
that depends on each via an in-repo relative file: path (validated at
resolution). Because the dependency lives inside the repo, it resolves the same
wherever the produced tree goes — the run container, the validation host, and any
clone of the published source repo — so a packages game both validates and stays
playable/buildable after release, with no absolute path to break.
The first consumer is the Spectra case, which loads its produced spectra-burst
system.json as a drone-pop effect. See
Packages.
The console renders every asset kind — live and after the run
Section titled “The console renders every asset kind — live and after the run”The UI library gains a lazy-loaded VoxelViewer (a
React Three Fiber canvas mounting the runtime’s VoxelRig): a voxel-model
auto-rotates; a voxel-animation gives one orbit-drag viewer per animation with a
range control per caller joint and playback of each auto-play joint, and a
[[review_item]] can name the caller joints it is about so the UI surfaces exactly
that control beside the item. Where WebGL is unavailable or reduced motion is
requested it falls back to the regenerated preview PNG. Post-run viewers gain
expand-to-fullscreen.
The live run monitor now shows work as it is produced, over one backward-compatible typed-body channel that streams each asset’s editable source after its preview PNG:
- Voxel / meshed runs rebuild in 3D and rotate as they are sculpted, with a “Scene” view (every part assembled at rest) and a per-part “Model” view.
- Particle runs mount the live simulator to play the effect as it is authored.
- Skinned runs decode the streamed rig and auto-play the first animation so the skin deforms live.
- Audio runs play the streamed clip in an
<audio>element beside the waveform.
A broader sculpting vocabulary and compound joints
Section titled “A broader sculpting vocabulary and compound joints”Both voxel binaries gained five operations for a more complete toolkit —
fill-cylinder, fill-ellipsoid, replace-color, translate, and copy-box
(all clip at the volume edge and replay deterministically). A voxel-animation
joint can also carry a fixed compound mount — an offset (translation) and an
orient (Euler rotation about the pivot) applied in addition to its driven motion,
so a component can be mounted at a custom rotation and translation; a joint with
an empty driven range but a non-zero mount is a purely static attachment. An
all-zero mount is omitted, so rigs without one are unchanged.
Geometry is stored as binary glTF, never JSON
Section titled “Geometry is stored as binary glTF, never JSON”Following the principle that bulk numeric data must be binary and JSON is reserved
for metadata, a voxel/meshed run stores its geometry as binary glTF (.glb) —
one part per file (meshes/{part}.glb), or a single mesh.glb for a static or
skinned model — with rig.json carrying only the rig contract. The intermediate
voxels.json and mesh.json payloads are gone; the .glb is the sole geometry,
read directly by the TypeScript runtime, the review UI, the exporter, and the
validator.
Variant files, per-variant domains, and per-variant volumes
Section titled “Variant files, per-variant domains, and per-variant volumes”The manifest format for variants was reworked across every case. Variants move out
of inline [[variant]] tables into their own variants/<slug>.toml files,
declared by an ordered root-level variants = [...] list (the first entry is the
default); inline variants are no longer supported. A variant may declare its own
scoring [[domain]]s — a case declares the common domains every variant is rated
on, and a run is scored against common + its variant’s own — and a voxel
variant may declare its own [voxel] table that overrides the case’s for that
variant’s runs. Prompt and spec templates gain a {{voxel}} context
(width/height/depth and maxX/maxY/maxZ) so a brief states its volume
from one source of truth, and a [[spec]] dest now defaults to its source (with
.hbs stripped). A shared quality directive is prepended to every
asset-generation prompt.
Per-version changelogs
Section titled “Per-version changelogs”Every test-case version now declares a required changelog.md recording what
changed in it, threaded through the whole data contract (core resolve → backend
store/ingest → API and snapshot → generated TS/JSON-Schema → both frontends). The
catalog aggregates every version’s entry into a new “Changelog” tab on the case
detail page, newest-first, and the console now sorts versions newest-first.
GIF export for animations
Section titled “GIF export for animations”An asset-generation run’s Verdict tab can download a GIF of a sprite-sheet
sequence or a voxel animation. The sprite-sheet path reuses the exact frames the
player draws, upscaled nearest-neighbor and quantized with gifenc preserving the
transparent background; the voxel path bakes an autoplaying clip on a throwaway
offscreen three.js renderer that mirrors the preview’s camera and lighting, stepping
the rig deterministically via a new VoxelRig.seek(timeMs). Both share a reusable
GifDownloadButton that works in the web console and the desktop webview.
Proof clips are captured as .webm, transcoded to .mp4 for the gallery
Section titled “Proof clips are captured as .webm, transcoded to .mp4 for the gallery”An end-to-end case’s video
proof is now captured as the .webm
Playwright records natively, so a run writes its clip with
no in-container conversion. The public gallery
transcodes each clip to H.264 .mp4 with ffmpeg when it builds the snapshot (webm/VP8
does not play on iOS/Safari), rewriting the moov atom to the front for progressive
playback and falling back to the raw webm if a transcode fails; the live console
serves the raw .webm directly. A proof dest now accepts webm in addition to
mp4.
Publishing is an asynchronous, in-cluster Job
Section titled “Publishing is an asynchronous, in-cluster Job”POST /runs/{id}/publish no longer flips a run public synchronously. It now gates
and enqueues — checking the run is a completed, non-infrastructure-failure run
carrying at least one review, inserting a publish_job row with a per-job token,
and returning 202 Accepted with a live URL — and the release runs entirely in
the cluster, mirroring how a run executes:
- The dispatcher drains the publish queue each tick (after the run queue), claiming a job over a service-token route.
- It builds one minimal
tcab-publisherJob per claim — no sandbox, no subscription volume, no ServiceAccount (the publisher is HTTP-only and never creates pods). - The publisher pod (no local checkout) downloads the run’s source tree from
the artifact service’s
GET /runs/{id}/tree.tar, performs the GitHub-repo creation andwrangler pages deploy, and streams progress lines back to the backend. - On success the backend patches the run’s links, flips it
published, and regenerates the public snapshot; on failure it records the reason.
tcab publish and the console both consume that live stream instead of blocking
on a synchronous call. The new tcab-publisher crate (crates/publisher/) builds
to its own image that drops the driver’s Playwright/Chromium layer (it renders
nothing). See results and
artifacts.
azure-prod is a reachable production environment
Section titled “azure-prod is a reachable production environment”With the last off-cluster pieces gone, several parts land together to make the Azure prod overlay a real environment a VPN client can reach and an operator can keep running:
- A private, never-public ingress. A reusable
internal-ingresscomponent exposes the console and the backend services over a VPN-only internal load balancer under a dedicatedtcab.testcabinet.aisubdomain, with TLS issued automatically by cert-manager via a Let’s Encrypt DNS-01 challenge through Cloudflare. Only the A records are private, so the subdomain does not shadow the public gallery and docs. - All secrets from Azure Key Vault. A
keyvault-csicomponent mirrors vault secrets into native Kubernetes Secrets, authenticated by Azure Workload Identity; auto-rotation reconciles a changed value on its own, which matters because the harness subscription tokens (Claude Code’s expires ~daily) are uploaded to the vault and mounted read-only into each driver Job. - A self-healing catalog. Because the managed-Postgres overlay gives the backend
an ephemeral def store, catalog ingest runs as a one-shot sidecar in the backend
pod that clones the repo and POSTs
/ingestover localhost on start; catalog changes are published on demand withscripts/reingest-prod.shviaaz aks command invoke, no local kubeconfig needed.
See deployment.
The web console ships in-cluster everywhere
Section titled “The web console ships in-cluster everywhere”apps/web is now packaged as a static nginx image configured at runtime —
an entrypoint renders /config.js from TCAB_WEB_BACKEND_URL / TCAB_WEB_AUTH_URL,
so one environment-agnostic image per git-sha is pointed at each environment by env
var. A reusable Deployment+Service is nested into both the local and prod overlays.
Locally, make local-up now brings the entire UI up in the cluster and forwards
it — there is no separate npm run dev and no VITE_BACKEND_URL to set. See
Running the local service stack.
Run-container images are pinnable by git-sha and multi-arch
Section titled “Run-container images are pinnable by git-sha and multi-arch”The run-container images are now published multi-arch (linux/amd64 +
linux/arm64, each built on a native runner and fused into a manifest list) — the
deployments run on arm64 nodes, where an amd64-only image made every run pod die
with exec format error. CI also tags an immutable :<git-sha> manifest list
beside :latest, and because run-container images aren’t Kubernetes image: fields
(the driver resolves them at runtime), the dispatcher forwards TCAB_CONTAINER_TAG
and the per-image overrides into every driver Job, so a cluster can pin or roll back
its run images on the same staging → prod sha flow as the service images. A new
guide documents the rollout.
Catalog ingest is atomic
Section titled “Catalog ingest is atomic”A force re-ingest used to rebuild a version in place, leaving it manifest-less
for the seconds-to-minutes the reference mockups take to render, so any run
resolving that version mid-re-ingest got a 404 "is not ingested". Each version is
now built in a staging dir and swapped into place with a rename, so a concurrent
resolve always sees either the old or the new version, never a half-built tree —
which is what makes the on-demand prod reingest safe to run while runs execute. A
companion optimization only re-ingests test cases changed since the last run.
The run review UI is rebuilt around individual reviews
Section titled “The run review UI is rebuilt around individual reviews”A run’s individual reviews are now first-class, linkable surfaces instead of being
collapsed into one aggregate verdict. The Verdict tab still shows the aggregate
verdict, but beneath it a review list shows each
review as a compact, clickable card that opens its own page at
/runs/:runId/reviews/:reviewerId; the Edit control moves onto a reviewer’s own
review page.
Killing an in-progress run
Section titled “Killing an in-progress run”A queued or running run can be canceled end to end. POST /jobs/{id}/cancel
(gated on the launching account) atomically moves a non-terminal job to canceled
and closes its live stream so every watching monitor ends at once; the driver polls
its own job state and, on canceled, drops the in-flight harness exec and tears
down its sandbox (under Kubernetes deleting the sandbox pod found by a
tcab.dev/job-id label) without reporting. A “Kill run” control appears in the
live monitor for anyone with execution rights.
Sortable, configurable, and resizable catalog tables
Section titled “Sortable, configurable, and resizable catalog tables”The run-log and models tables became descriptor-driven, so their columns can be sorted (click a header to cycle default → ascending → descending, persisted per scope and applied across the whole run history before paging), shown or hidden (the run log gains optional Timestamp, Category, and Duration columns via a picker), and resized (drag a column edge; widths persist to localStorage, keyed by column id and floored at the header label width).
Three new models
Section titled “Three new models”The catalog adds Claude Sonnet 5 (Anthropic), Devstral 2512 (Mistral, a coding-specialized model), and Mistral Medium 3.5 (Mistral).
A wave of new test cases
Section titled “A wave of new test cases”Local development runs the whole stack inside the devcontainer against the host Docker daemon, and against it a large set of new cases landed:
- New end-to-end games, including Siege (the first 3D/voxel FPS last-stand
case), Caldera (a hex-terrain tower defense with a water→steam fluid sim),
Thunderhead (a multi-faction fleet-command case), Sunfront (a tug-of-war
RTS with the Duneforged voxel asset family), Floe, Wireworm / Fireworm,
and Shatter.
- Caldera, Siege, Thunderhead, and Sunfront should be considered pending tests, not tests ready for use. Their assets still need to be generated by asset generation test cases, and the specs still need to be revised to address issues that will be obvious once a first implementation is created.
- The first case for each new asset kind — UI, material, skinned, particle, and audio — alongside the meshing sample cases and the Sunfront/Aegis 3D families.
- Existing “inspired-by” slugs were renamed to Test Cabinet names (Cascade, Carom, Coil, Fathom, Lattice, Meltdown), and end-to-end difficulty was recalibrated by dimension.
New authoring guides cover every new kind
(voxel models and animations,
meshed and
skinned models,
UI,
materials,
particles, and
audio); the authoring and variant
procedures now live in the docs site rather than in repo skills, with a single
authoring-test-cases skill routing to the right guide.
Runs queue for capacity instead of failing
Section titled “Runs queue for capacity instead of failing”When the cluster had no free capacity, a sandbox pod sat Pending and the driver
failed the run after a single 180s deadline — reporting a healthy run merely waiting
its turn as an infrastructure failure. The pod wait is now two phases: a
scheduling wait (until the pod is bound to a node) bounded only by an opt-in
timeout that defaults to waiting forever, so a busy cluster makes runs queue; and the
existing tight startup timeout once scheduled, so a genuinely broken pod still
fails fast. Time spent queued is subtracted from the run’s recorded duration. See
Kubernetes.
Run-artifact collection survives a torn exec channel
Section titled “Run-artifact collection survives a torn exec channel”The driver collects a finished run’s tree by streaming tar -c /work over the kube
exec WebSocket and reading the exit code from the channel’s terminating Status
frame. When a managed API-server tunnel severed that long-lived stream, no Status
arrived, so the exit came back as -1 even though the tree was intact — and because
the dispatcher never retries a driver Job, a single transport blip permanently failed
an otherwise-complete run. Collection (a read-only tar) is now retried up to four
times with exponential backoff.
Publishing reliability
Section titled “Publishing reliability”The new publish path hit several real-world failures, now handled: GitHub
post-create permission lag (repo creation and the push are now separate steps, the
push retries with backoff, and release_code is idempotent); opaque deploy
failures (wrangler diagnostics are now drawn from both stdout and stderr); and
Cloudflare account discovery (the Job now provides CLOUDFLARE_ACCOUNT_ID so
wrangler skips discovery).
The backend can reach the auth service
Section titled “The backend can reach the auth service”The base NetworkPolicy admitted the auth service from the dispatcher, driver, artifacts, and arena — but not from the backend itself, which verifies every bearer token in-cluster. Under default-deny ingress that verify call timed out and run creation failed with a 502. The policy now includes the backend as an allowed source.
Deleting a run cleans up its queue rows
Section titled “Deleting a run cleans up its queue rows”delete_run removed the run and its cascading reviews and links but left the
run-queue and publish-queue rows behind — both reference the run by a plain column
with no foreign key, so nothing cascaded. Both queue rows are now deleted in the same
transaction.
In-progress runs reconcile without a refresh
Section titled “In-progress runs reconcile without a refresh”The web console now reconciles in-progress runs so a completion surfaces on its own, without a manual refresh.
Console and viewer polish
Section titled “Console and viewer polish”- The voxel rig reviewer was reworked (mode switch above the split so it stops overflowing the sidebar, real joint counts, geometry stats, scrolling), the preview was enlarged and framed to fill the view instead of half of it, and it no longer vanishes under StrictMode. Positive pitch now elevates a forward part, and the caller-joint slider no longer pops a validity bubble.
- Particle previews frame and scroll/zoom correctly.
- The run-detail pages no longer crash on the static public gallery
(
RunDeleteControlis now gated at its mount site), review cards drop the global hover underline, the active tab label regains its readability halo, and the console tabs show the cabinet favicon. - Model prices show four decimals only for sub-dollar USD figures, and the
openrouter/prefix is stripped when processing model ids.