Aero chat welcome panel: frosted and flush; honest model availability

The welcome panel is the same frosted glass as the cards now (blurred,
not a flat tint) and stretches flush with the chat header and composer
(measured: 268..1280 at 1280, exactly the chrome's edges) - a flat pane
floating 40px inside the scene read as "cut off next to the sidebar".

Availability no longer over-promises: a /api/tags listing proves
availability only on a local daemon (the listing is the installed set);
on a cloud key the catalogue can include models a free plan cannot run,
and provider seeds before their first live /models test, so those stay
badge-less until actually used. The configured favourites keep their
verified state.

Contrast 0 findings, chat 102/102, providers 20/20, mobile 126/126,
i18n 13/13, native 33/33. Version 0.5.2, SW -> freezmo-v75.
This commit is contained in:
Martin Dvořák 2026-10-14 08:51:14 +00:00
commit 7e7969b568
4 changed files with 25 additions and 5 deletions

View file

@ -153,7 +153,14 @@ export async function GET(request: Request) {
if (known.has(canonical)) continue;
if (/embed|bert|whisper|clip|rerank/i.test(model.name)) continue;
known.add(canonical);
extras.push({ name: model.name, badge: badgeFor(model.name), available: true });
// A local daemon's listing *is* its installed set, so "available" is a
// fact there. The cloud's /api/tags is a catalogue — a free key can list
// models its plan cannot run — so those stay "unknown" until used.
extras.push({
name: model.name,
badge: badgeFor(model.name),
available: health.mode === "local-daemon" ? true : null,
});
}
extras.sort((a, b) => a.name.localeCompare(b.name));
health.chatModels = [...health.chatModels, ...extras.slice(0, 120)];

View file

@ -442,6 +442,7 @@
.aero .glass-pill,
.aero .btn-secondary,
.aero .chat-header,
.aero .chat-empty,
.aero .segmented,
/* Hand-rolled frost fills in the study, exam, chat and homework flows:
white/xx utilities and the surface tokens — without this they render as
@ -621,6 +622,7 @@
.aero .glass-pill,
.aero .btn-secondary,
.aero .chat-header,
.aero .chat-empty,
.aero .segmented {
background-image: none;
background-color: rgb(255 255 255 / 0.32);
@ -664,14 +666,22 @@
box-shadow: 0 -16px 36px -26px rgb(2 62 110 / 0.5);
}
/* The chat welcome (subtitle + suggestions) gets a soft panel, so clouds and
bubbles never sit right behind the text. */
/* The chat welcome (subtitle + suggestions): the same frosted glass as the
cards (the shared recipe above), stretched flush with the header and the
composer — a panel floating 40px inside the scene looked cut off. */
.aero .chat-empty {
margin-inline: -1rem;
border-radius: 1.5rem;
border: 1px solid rgb(255 255 255 / 0.5);
background-color: rgb(255 255 255 / 0.32);
padding: 1.35rem 1rem 1.1rem;
}
@media (min-width: 1024px) {
.aero .chat-empty {
margin-inline: -2.5rem;
padding: 1.5rem 2.5rem 1.25rem;
}
}
/* Page headings that sit straight on the wallpaper. */
.aero .page-heading {

View file

@ -179,8 +179,11 @@ export function ChatScreen() {
if (!providerState.keys[info.id]) continue;
const models = modelsFor(providerState, info.id).slice(0, 400);
if (models.length === 0) continue;
// Only a live /models listing proves the key can use them; the built-in
// seeds stay badge-less until the first successful test.
const listed = (providerState.models[info.id]?.length ?? 0) > 0;
for (const name of models) {
cloud.push({ name, badge: "text", available: true, provider: info.id });
cloud.push({ name, badge: "text", available: listed ? true : null, provider: info.id });
}
}
return [...ollama, ...cloud];

View file

@ -1,6 +1,6 @@
{
"name": "freezmo",
"version": "0.5.0",
"version": "0.5.2",
"private": true,
"license": "GPL-3.0-only",
"scripts": {